capybara 3.32.2
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 +7 -0
- data/.yardopts +1 -0
- data/History.md +1813 -0
- data/License.txt +22 -0
- data/README.md +1099 -0
- data/lib/capybara.rb +511 -0
- data/lib/capybara/config.rb +94 -0
- data/lib/capybara/cucumber.rb +27 -0
- data/lib/capybara/driver/base.rb +170 -0
- data/lib/capybara/driver/node.rb +139 -0
- data/lib/capybara/dsl.rb +65 -0
- data/lib/capybara/helpers.rb +108 -0
- data/lib/capybara/minitest.rb +386 -0
- data/lib/capybara/minitest/spec.rb +264 -0
- data/lib/capybara/node/actions.rb +420 -0
- data/lib/capybara/node/base.rb +143 -0
- data/lib/capybara/node/document.rb +48 -0
- data/lib/capybara/node/document_matchers.rb +67 -0
- data/lib/capybara/node/element.rb +606 -0
- data/lib/capybara/node/finders.rb +325 -0
- data/lib/capybara/node/matchers.rb +883 -0
- data/lib/capybara/node/simple.rb +208 -0
- data/lib/capybara/queries/ancestor_query.rb +27 -0
- data/lib/capybara/queries/base_query.rb +106 -0
- data/lib/capybara/queries/current_path_query.rb +51 -0
- data/lib/capybara/queries/match_query.rb +26 -0
- data/lib/capybara/queries/selector_query.rb +710 -0
- data/lib/capybara/queries/sibling_query.rb +26 -0
- data/lib/capybara/queries/style_query.rb +45 -0
- data/lib/capybara/queries/text_query.rb +110 -0
- data/lib/capybara/queries/title_query.rb +39 -0
- data/lib/capybara/rack_test/browser.rb +140 -0
- data/lib/capybara/rack_test/css_handlers.rb +13 -0
- data/lib/capybara/rack_test/driver.rb +109 -0
- data/lib/capybara/rack_test/errors.rb +6 -0
- data/lib/capybara/rack_test/form.rb +127 -0
- data/lib/capybara/rack_test/node.rb +325 -0
- data/lib/capybara/rails.rb +16 -0
- data/lib/capybara/registrations/drivers.rb +36 -0
- data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
- data/lib/capybara/registrations/servers.rb +44 -0
- data/lib/capybara/result.rb +190 -0
- data/lib/capybara/rspec.rb +29 -0
- data/lib/capybara/rspec/features.rb +23 -0
- data/lib/capybara/rspec/matcher_proxies.rb +82 -0
- data/lib/capybara/rspec/matchers.rb +201 -0
- data/lib/capybara/rspec/matchers/base.rb +111 -0
- data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
- data/lib/capybara/rspec/matchers/compound.rb +88 -0
- 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 +29 -0
- data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
- data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
- data/lib/capybara/rspec/matchers/have_text.rb +33 -0
- data/lib/capybara/rspec/matchers/have_title.rb +29 -0
- data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
- data/lib/capybara/rspec/matchers/match_style.rb +38 -0
- data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
- data/lib/capybara/selector.rb +233 -0
- data/lib/capybara/selector/builders/css_builder.rb +84 -0
- data/lib/capybara/selector/builders/xpath_builder.rb +69 -0
- data/lib/capybara/selector/css.rb +102 -0
- data/lib/capybara/selector/definition.rb +276 -0
- data/lib/capybara/selector/definition/button.rb +51 -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 +27 -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 +46 -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/filter.rb +5 -0
- data/lib/capybara/selector/filter_set.rb +124 -0
- data/lib/capybara/selector/filters/base.rb +77 -0
- data/lib/capybara/selector/filters/expression_filter.rb +22 -0
- data/lib/capybara/selector/filters/locator_filter.rb +29 -0
- data/lib/capybara/selector/filters/node_filter.rb +31 -0
- data/lib/capybara/selector/regexp_disassembler.rb +214 -0
- data/lib/capybara/selector/selector.rb +147 -0
- data/lib/capybara/selector/xpath_extensions.rb +17 -0
- 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 +496 -0
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +119 -0
- data/lib/capybara/selenium/driver_specializations/edge_driver.rb +126 -0
- data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
- 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 +110 -0
- data/lib/capybara/selenium/extensions/html5_drag.rb +228 -0
- data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
- data/lib/capybara/selenium/extensions/scroll.rb +78 -0
- data/lib/capybara/selenium/logger_suppressor.rb +34 -0
- data/lib/capybara/selenium/node.rb +610 -0
- data/lib/capybara/selenium/nodes/chrome_node.rb +119 -0
- data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
- data/lib/capybara/selenium/nodes/firefox_node.rb +131 -0
- 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/action_pauser.rb +26 -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 +47 -0
- data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
- data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
- data/lib/capybara/server.rb +126 -0
- data/lib/capybara/server/animation_disabler.rb +58 -0
- data/lib/capybara/server/checker.rb +44 -0
- data/lib/capybara/server/middleware.rb +69 -0
- data/lib/capybara/session.rb +942 -0
- data/lib/capybara/session/config.rb +124 -0
- data/lib/capybara/session/matchers.rb +87 -0
- data/lib/capybara/spec/fixtures/another_test_file.txt +1 -0
- data/lib/capybara/spec/fixtures/capybara.jpg +3 -0
- data/lib/capybara/spec/fixtures/no_extension +1 -0
- data/lib/capybara/spec/fixtures/test_file.txt +1 -0
- data/lib/capybara/spec/public/jquery-ui.js +13 -0
- data/lib/capybara/spec/public/jquery.js +5 -0
- data/lib/capybara/spec/public/offset.js +6 -0
- data/lib/capybara/spec/public/test.js +268 -0
- data/lib/capybara/spec/session/accept_alert_spec.rb +81 -0
- data/lib/capybara/spec/session/accept_confirm_spec.rb +32 -0
- data/lib/capybara/spec/session/accept_prompt_spec.rb +78 -0
- data/lib/capybara/spec/session/all_spec.rb +278 -0
- data/lib/capybara/spec/session/ancestor_spec.rb +88 -0
- data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +140 -0
- data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
- data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
- data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
- data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
- data/lib/capybara/spec/session/assert_title_spec.rb +93 -0
- data/lib/capybara/spec/session/attach_file_spec.rb +216 -0
- data/lib/capybara/spec/session/body_spec.rb +23 -0
- data/lib/capybara/spec/session/check_spec.rb +235 -0
- data/lib/capybara/spec/session/choose_spec.rb +121 -0
- data/lib/capybara/spec/session/click_button_spec.rb +506 -0
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +129 -0
- data/lib/capybara/spec/session/click_link_spec.rb +229 -0
- data/lib/capybara/spec/session/current_scope_spec.rb +31 -0
- data/lib/capybara/spec/session/current_url_spec.rb +115 -0
- data/lib/capybara/spec/session/dismiss_confirm_spec.rb +36 -0
- data/lib/capybara/spec/session/dismiss_prompt_spec.rb +21 -0
- data/lib/capybara/spec/session/element/assert_match_selector_spec.rb +38 -0
- data/lib/capybara/spec/session/element/match_css_spec.rb +31 -0
- data/lib/capybara/spec/session/element/match_xpath_spec.rb +25 -0
- data/lib/capybara/spec/session/element/matches_selector_spec.rb +120 -0
- data/lib/capybara/spec/session/evaluate_async_script_spec.rb +23 -0
- data/lib/capybara/spec/session/evaluate_script_spec.rb +49 -0
- data/lib/capybara/spec/session/execute_script_spec.rb +28 -0
- data/lib/capybara/spec/session/fill_in_spec.rb +286 -0
- data/lib/capybara/spec/session/find_button_spec.rb +74 -0
- data/lib/capybara/spec/session/find_by_id_spec.rb +33 -0
- data/lib/capybara/spec/session/find_field_spec.rb +113 -0
- data/lib/capybara/spec/session/find_link_spec.rb +70 -0
- data/lib/capybara/spec/session/find_spec.rb +531 -0
- data/lib/capybara/spec/session/first_spec.rb +156 -0
- data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
- data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
- data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +116 -0
- data/lib/capybara/spec/session/frame/within_frame_spec.rb +112 -0
- data/lib/capybara/spec/session/go_back_spec.rb +12 -0
- data/lib/capybara/spec/session/go_forward_spec.rb +14 -0
- data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
- data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
- data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
- data/lib/capybara/spec/session/has_button_spec.rb +69 -0
- data/lib/capybara/spec/session/has_css_spec.rb +374 -0
- data/lib/capybara/spec/session/has_current_path_spec.rb +138 -0
- data/lib/capybara/spec/session/has_field_spec.rb +349 -0
- data/lib/capybara/spec/session/has_link_spec.rb +39 -0
- data/lib/capybara/spec/session/has_none_selectors_spec.rb +78 -0
- data/lib/capybara/spec/session/has_select_spec.rb +310 -0
- data/lib/capybara/spec/session/has_selector_spec.rb +202 -0
- data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
- data/lib/capybara/spec/session/has_table_spec.rb +198 -0
- data/lib/capybara/spec/session/has_text_spec.rb +394 -0
- data/lib/capybara/spec/session/has_title_spec.rb +71 -0
- data/lib/capybara/spec/session/has_xpath_spec.rb +149 -0
- data/lib/capybara/spec/session/headers_spec.rb +8 -0
- data/lib/capybara/spec/session/html_spec.rb +47 -0
- data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
- data/lib/capybara/spec/session/node_spec.rb +1292 -0
- data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
- data/lib/capybara/spec/session/refresh_spec.rb +33 -0
- data/lib/capybara/spec/session/reset_session_spec.rb +148 -0
- data/lib/capybara/spec/session/response_code_spec.rb +8 -0
- data/lib/capybara/spec/session/save_and_open_page_spec.rb +21 -0
- data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +43 -0
- data/lib/capybara/spec/session/save_page_spec.rb +110 -0
- data/lib/capybara/spec/session/save_screenshot_spec.rb +55 -0
- data/lib/capybara/spec/session/screenshot_spec.rb +18 -0
- data/lib/capybara/spec/session/scroll_spec.rb +117 -0
- data/lib/capybara/spec/session/select_spec.rb +229 -0
- data/lib/capybara/spec/session/selectors_spec.rb +98 -0
- data/lib/capybara/spec/session/sibling_spec.rb +52 -0
- data/lib/capybara/spec/session/source_spec.rb +0 -0
- data/lib/capybara/spec/session/text_spec.rb +74 -0
- data/lib/capybara/spec/session/title_spec.rb +29 -0
- data/lib/capybara/spec/session/uncheck_spec.rb +100 -0
- data/lib/capybara/spec/session/unselect_spec.rb +116 -0
- data/lib/capybara/spec/session/visit_spec.rb +204 -0
- data/lib/capybara/spec/session/window/become_closed_spec.rb +89 -0
- data/lib/capybara/spec/session/window/current_window_spec.rb +28 -0
- data/lib/capybara/spec/session/window/open_new_window_spec.rb +31 -0
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +132 -0
- data/lib/capybara/spec/session/window/window_opened_by_spec.rb +99 -0
- data/lib/capybara/spec/session/window/window_spec.rb +203 -0
- data/lib/capybara/spec/session/window/windows_spec.rb +34 -0
- data/lib/capybara/spec/session/window/within_window_spec.rb +157 -0
- data/lib/capybara/spec/session/within_spec.rb +199 -0
- data/lib/capybara/spec/spec_helper.rb +134 -0
- data/lib/capybara/spec/test_app.rb +226 -0
- data/lib/capybara/spec/views/animated.erb +49 -0
- data/lib/capybara/spec/views/buttons.erb +5 -0
- data/lib/capybara/spec/views/fieldsets.erb +30 -0
- data/lib/capybara/spec/views/form.erb +685 -0
- data/lib/capybara/spec/views/frame_child.erb +18 -0
- data/lib/capybara/spec/views/frame_one.erb +10 -0
- data/lib/capybara/spec/views/frame_parent.erb +9 -0
- data/lib/capybara/spec/views/frame_two.erb +9 -0
- data/lib/capybara/spec/views/header_links.erb +8 -0
- data/lib/capybara/spec/views/host_links.erb +13 -0
- data/lib/capybara/spec/views/initial_alert.erb +10 -0
- data/lib/capybara/spec/views/obscured.erb +47 -0
- data/lib/capybara/spec/views/offset.erb +32 -0
- data/lib/capybara/spec/views/path.erb +13 -0
- data/lib/capybara/spec/views/popup_one.erb +9 -0
- data/lib/capybara/spec/views/popup_two.erb +9 -0
- data/lib/capybara/spec/views/postback.erb +14 -0
- data/lib/capybara/spec/views/react.erb +45 -0
- data/lib/capybara/spec/views/scroll.erb +20 -0
- data/lib/capybara/spec/views/spatial.erb +31 -0
- data/lib/capybara/spec/views/tables.erb +130 -0
- data/lib/capybara/spec/views/with_animation.erb +74 -0
- data/lib/capybara/spec/views/with_base_tag.erb +11 -0
- data/lib/capybara/spec/views/with_count.erb +8 -0
- data/lib/capybara/spec/views/with_dragula.erb +22 -0
- data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
- data/lib/capybara/spec/views/with_hover.erb +24 -0
- data/lib/capybara/spec/views/with_hover1.erb +10 -0
- data/lib/capybara/spec/views/with_html.erb +208 -0
- data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
- data/lib/capybara/spec/views/with_html_entities.erb +2 -0
- data/lib/capybara/spec/views/with_js.erb +160 -0
- data/lib/capybara/spec/views/with_jstree.erb +26 -0
- data/lib/capybara/spec/views/with_namespace.erb +20 -0
- data/lib/capybara/spec/views/with_scope.erb +42 -0
- data/lib/capybara/spec/views/with_scope_other.erb +6 -0
- data/lib/capybara/spec/views/with_simple_html.erb +2 -0
- data/lib/capybara/spec/views/with_slow_unload.erb +17 -0
- data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
- data/lib/capybara/spec/views/with_title.erb +5 -0
- data/lib/capybara/spec/views/with_unload_alert.erb +14 -0
- data/lib/capybara/spec/views/with_windows.erb +54 -0
- data/lib/capybara/spec/views/within_frames.erb +15 -0
- data/lib/capybara/version.rb +5 -0
- data/lib/capybara/window.rb +146 -0
- data/spec/basic_node_spec.rb +154 -0
- data/spec/capybara_spec.rb +112 -0
- data/spec/css_builder_spec.rb +101 -0
- data/spec/css_splitter_spec.rb +38 -0
- data/spec/dsl_spec.rb +276 -0
- data/spec/filter_set_spec.rb +46 -0
- data/spec/fixtures/capybara.csv +1 -0
- data/spec/fixtures/certificate.pem +25 -0
- data/spec/fixtures/key.pem +27 -0
- data/spec/fixtures/selenium_driver_rspec_failure.rb +13 -0
- data/spec/fixtures/selenium_driver_rspec_success.rb +13 -0
- data/spec/minitest_spec.rb +163 -0
- data/spec/minitest_spec_spec.rb +162 -0
- data/spec/per_session_config_spec.rb +68 -0
- data/spec/rack_test_spec.rb +268 -0
- data/spec/regexp_dissassembler_spec.rb +250 -0
- data/spec/result_spec.rb +196 -0
- data/spec/rspec/features_spec.rb +99 -0
- data/spec/rspec/scenarios_spec.rb +19 -0
- data/spec/rspec/shared_spec_matchers.rb +947 -0
- data/spec/rspec/views_spec.rb +14 -0
- data/spec/rspec_matchers_spec.rb +62 -0
- data/spec/rspec_spec.rb +145 -0
- data/spec/sauce_spec_chrome.rb +43 -0
- data/spec/selector_spec.rb +513 -0
- data/spec/selenium_spec_chrome.rb +188 -0
- data/spec/selenium_spec_chrome_remote.rb +96 -0
- data/spec/selenium_spec_edge.rb +47 -0
- data/spec/selenium_spec_firefox.rb +208 -0
- data/spec/selenium_spec_firefox_remote.rb +80 -0
- data/spec/selenium_spec_ie.rb +150 -0
- data/spec/selenium_spec_safari.rb +148 -0
- data/spec/server_spec.rb +292 -0
- data/spec/session_spec.rb +91 -0
- data/spec/shared_selenium_node.rb +83 -0
- data/spec/shared_selenium_session.rb +476 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/xpath_builder_spec.rb +93 -0
- metadata +753 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Capybara::Selenium::Node
|
|
4
|
+
module Html5Drag
|
|
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)
|
|
9
|
+
|
|
10
|
+
driver.execute_script MOUSEDOWN_TRACKER
|
|
11
|
+
scroll_if_needed { browser_action.click_and_hold(native).perform }
|
|
12
|
+
html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
|
|
13
|
+
if html5
|
|
14
|
+
perform_html5_drag(element, delay, drop_modifiers)
|
|
15
|
+
else
|
|
16
|
+
perform_legacy_drag(element, drop_modifiers)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
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.each_with_object([]) do |arg, arr|
|
|
43
|
+
arg.each_with_object(arr) do |(type, data), arr_|
|
|
44
|
+
arr_ << { type: type, data: data }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
driver.execute_script DROP_STRING, items, self
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
DROP_STRING = <<~JS
|
|
52
|
+
var strings = arguments[0],
|
|
53
|
+
el = arguments[1],
|
|
54
|
+
dt = new DataTransfer(),
|
|
55
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
56
|
+
for (var i=0; i < strings.length; i++){
|
|
57
|
+
if (dt.items) {
|
|
58
|
+
dt.items.add(strings[i]['data'], strings[i]['type']);
|
|
59
|
+
} else {
|
|
60
|
+
dt.setData(strings[i]['type'], strings[i]['data']);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var dropEvent = new DragEvent('drop', opts);
|
|
64
|
+
el.dispatchEvent(dropEvent);
|
|
65
|
+
JS
|
|
66
|
+
|
|
67
|
+
DROP_FILE = <<~JS
|
|
68
|
+
var el = arguments[0],
|
|
69
|
+
input = arguments[1],
|
|
70
|
+
files = input.files,
|
|
71
|
+
dt = new DataTransfer(),
|
|
72
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
73
|
+
input.parentElement.removeChild(input);
|
|
74
|
+
if (dt.items){
|
|
75
|
+
for (var i=0; i<files.length; i++){
|
|
76
|
+
dt.items.add(files[i]);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
Object.defineProperty(dt, "files", {
|
|
80
|
+
value: files,
|
|
81
|
+
writable: false
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
var dropEvent = new DragEvent('drop', opts);
|
|
85
|
+
el.dispatchEvent(dropEvent);
|
|
86
|
+
JS
|
|
87
|
+
|
|
88
|
+
ATTACH_FILE = <<~JS
|
|
89
|
+
(function(){
|
|
90
|
+
var input = document.createElement('INPUT');
|
|
91
|
+
input.type = "file";
|
|
92
|
+
input.id = "_capybara_drop_file";
|
|
93
|
+
input.multiple = true;
|
|
94
|
+
document.body.appendChild(input);
|
|
95
|
+
return input;
|
|
96
|
+
})()
|
|
97
|
+
JS
|
|
98
|
+
|
|
99
|
+
MOUSEDOWN_TRACKER = <<~JS
|
|
100
|
+
window.capybara_mousedown_prevented = null;
|
|
101
|
+
document.addEventListener('mousedown', ev => {
|
|
102
|
+
window.capybara_mousedown_prevented = ev.defaultPrevented;
|
|
103
|
+
}, { once: true, passive: true })
|
|
104
|
+
JS
|
|
105
|
+
|
|
106
|
+
LEGACY_DRAG_CHECK = <<~JS
|
|
107
|
+
(function(el){
|
|
108
|
+
if ([true, null].indexOf(window.capybara_mousedown_prevented) >= 0){
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
do {
|
|
113
|
+
if (el.draggable) return false;
|
|
114
|
+
} while (el = el.parentElement );
|
|
115
|
+
return true;
|
|
116
|
+
})(arguments[0])
|
|
117
|
+
JS
|
|
118
|
+
|
|
119
|
+
HTML5_DRAG_DROP_SCRIPT = <<~JS
|
|
120
|
+
function rectCenter(rect){
|
|
121
|
+
return new DOMPoint(
|
|
122
|
+
(rect.left + rect.right)/2,
|
|
123
|
+
(rect.top + rect.bottom)/2
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pointOnRect(pt, rect) {
|
|
128
|
+
var rectPt = rectCenter(rect);
|
|
129
|
+
var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);
|
|
130
|
+
|
|
131
|
+
if (pt.x <= rectPt.x) { // left side
|
|
132
|
+
var minXy = slope * (rect.left - pt.x) + pt.y;
|
|
133
|
+
if (rect.top <= minXy && minXy <= rect.bottom)
|
|
134
|
+
return new DOMPoint(rect.left, minXy);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (pt.x >= rectPt.x) { // right side
|
|
138
|
+
var maxXy = slope * (rect.right - pt.x) + pt.y;
|
|
139
|
+
if (rect.top <= maxXy && maxXy <= rect.bottom)
|
|
140
|
+
return new DOMPoint(rect.right, maxXy);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (pt.y <= rectPt.y) { // top side
|
|
144
|
+
var minYx = (rectPt.top - pt.y) / slope + pt.x;
|
|
145
|
+
if (rect.left <= minYx && minYx <= rect.right)
|
|
146
|
+
return new DOMPoint(minYx, rect.top);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (pt.y >= rectPt.y) { // bottom side
|
|
150
|
+
var maxYx = (rect.bottom - pt.y) / slope + pt.x;
|
|
151
|
+
if (rect.left <= maxYx && maxYx <= rect.right)
|
|
152
|
+
return new DOMPoint(maxYx, rect.bottom);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return new DOMPoint(pt.x,pt.y);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function dragEnterTarget() {
|
|
159
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
|
160
|
+
var targetRect = target.getBoundingClientRect();
|
|
161
|
+
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
|
162
|
+
|
|
163
|
+
for (var i = 0; i < drop_modifier_keys.length; i++) {
|
|
164
|
+
key = drop_modifier_keys[i];
|
|
165
|
+
if (key == "control"){
|
|
166
|
+
key = "ctrl"
|
|
167
|
+
}
|
|
168
|
+
opts[key + 'Key'] = true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// fire 2 dragover events to simulate dragging with a direction
|
|
172
|
+
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
|
173
|
+
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
|
174
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
175
|
+
target.dispatchEvent(dragOverEvent);
|
|
176
|
+
window.setTimeout(dragOnTarget, step_delay);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function dragOnTarget() {
|
|
180
|
+
var targetCenter = rectCenter(target.getBoundingClientRect());
|
|
181
|
+
var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
|
182
|
+
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
|
183
|
+
target.dispatchEvent(dragOverEvent);
|
|
184
|
+
window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function dragLeave(drop, dragOverOpts) {
|
|
188
|
+
var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
|
|
189
|
+
var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
|
|
190
|
+
target.dispatchEvent(dragLeaveEvent);
|
|
191
|
+
if (drop) {
|
|
192
|
+
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
|
193
|
+
target.dispatchEvent(dropEvent);
|
|
194
|
+
}
|
|
195
|
+
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
|
196
|
+
source.dispatchEvent(dragEndEvent);
|
|
197
|
+
callback.call(true);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var source = arguments[0],
|
|
201
|
+
target = arguments[1],
|
|
202
|
+
step_delay = arguments[2],
|
|
203
|
+
drop_modifier_keys = arguments[3],
|
|
204
|
+
callback = arguments[4];
|
|
205
|
+
|
|
206
|
+
var dt = new DataTransfer();
|
|
207
|
+
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
|
208
|
+
|
|
209
|
+
while (source && !source.draggable) {
|
|
210
|
+
source = source.parentElement;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (source.tagName == 'A'){
|
|
214
|
+
dt.setData('text/uri-list', source.href);
|
|
215
|
+
dt.setData('text', source.href);
|
|
216
|
+
}
|
|
217
|
+
if (source.tagName == 'IMG'){
|
|
218
|
+
dt.setData('text/uri-list', source.src);
|
|
219
|
+
dt.setData('text', source.src);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var dragEvent = new DragEvent('dragstart', opts);
|
|
223
|
+
source.dispatchEvent(dragEvent);
|
|
224
|
+
|
|
225
|
+
window.setTimeout(dragEnterTarget, step_delay);
|
|
226
|
+
JS
|
|
227
|
+
end
|
|
228
|
+
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
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Selenium
|
|
5
|
+
module Scroll
|
|
6
|
+
def scroll_by(x, y)
|
|
7
|
+
driver.execute_script <<~JS, self, x, y
|
|
8
|
+
var el = arguments[0];
|
|
9
|
+
if (el.scrollBy){
|
|
10
|
+
el.scrollBy(arguments[1], arguments[2]);
|
|
11
|
+
} else {
|
|
12
|
+
el.scrollTop = el.scrollTop + arguments[2];
|
|
13
|
+
el.scrollLeft = el.scrollLeft + arguments[1];
|
|
14
|
+
}
|
|
15
|
+
JS
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scroll_to(element, location, position = nil)
|
|
19
|
+
# location, element = element, nil if element.is_a? Symbol
|
|
20
|
+
if element.is_a? Capybara::Selenium::Node
|
|
21
|
+
scroll_element_to_location(element, location)
|
|
22
|
+
elsif location.is_a? Symbol
|
|
23
|
+
scroll_to_location(location)
|
|
24
|
+
else
|
|
25
|
+
scroll_to_coords(*position)
|
|
26
|
+
end
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def scroll_element_to_location(element, location)
|
|
33
|
+
scroll_opts = case location
|
|
34
|
+
when :top
|
|
35
|
+
'true'
|
|
36
|
+
when :bottom
|
|
37
|
+
'false'
|
|
38
|
+
when :center
|
|
39
|
+
"{behavior: 'instant', block: 'center'}"
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
|
42
|
+
end
|
|
43
|
+
driver.execute_script <<~JS, element
|
|
44
|
+
arguments[0].scrollIntoView(#{scroll_opts})
|
|
45
|
+
JS
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
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
|
+
driver.execute_script <<~JS, self
|
|
58
|
+
if (arguments[0].scrollTo){
|
|
59
|
+
arguments[0].scrollTo(0, #{scroll_y});
|
|
60
|
+
} else {
|
|
61
|
+
arguments[0].scrollTop = #{scroll_y};
|
|
62
|
+
}
|
|
63
|
+
JS
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scroll_to_coords(x, y)
|
|
67
|
+
driver.execute_script <<~JS, self, x, y
|
|
68
|
+
if (arguments[0].scrollTo){
|
|
69
|
+
arguments[0].scrollTo(arguments[1], arguments[2]);
|
|
70
|
+
} else {
|
|
71
|
+
arguments[0].scrollTop = arguments[2];
|
|
72
|
+
arguments[0].scrollLeft = arguments[1];
|
|
73
|
+
}
|
|
74
|
+
JS
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Selenium
|
|
5
|
+
module DeprecationSuppressor
|
|
6
|
+
def initialize(*)
|
|
7
|
+
@suppress_for_capybara = false
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def deprecate(*)
|
|
12
|
+
super unless @suppress_for_capybara
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def suppress_deprecations
|
|
16
|
+
prev_suppress_for_capybara, @suppress_for_capybara = @suppress_for_capybara, true
|
|
17
|
+
yield
|
|
18
|
+
ensure
|
|
19
|
+
@suppress_for_capybara = prev_suppress_for_capybara
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ErrorSuppressor
|
|
24
|
+
def for_code(*)
|
|
25
|
+
::Selenium::WebDriver.logger.suppress_deprecations do
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Selenium::WebDriver::Logger.prepend Capybara::Selenium::DeprecationSuppressor
|
|
34
|
+
Selenium::WebDriver::Error.singleton_class.prepend Capybara::Selenium::ErrorSuppressor
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Selenium specific implementation of the Capybara::Driver::Node API
|
|
4
|
+
|
|
5
|
+
require 'capybara/selenium/extensions/find'
|
|
6
|
+
require 'capybara/selenium/extensions/scroll'
|
|
7
|
+
|
|
8
|
+
class Capybara::Selenium::Node < Capybara::Driver::Node
|
|
9
|
+
include Capybara::Selenium::Find
|
|
10
|
+
include Capybara::Selenium::Scroll
|
|
11
|
+
|
|
12
|
+
def visible_text
|
|
13
|
+
native.text
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def all_text
|
|
17
|
+
text = driver.evaluate_script('arguments[0].textContent', self)
|
|
18
|
+
text.gsub(/[\u200b\u200e\u200f]/, '')
|
|
19
|
+
.gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
|
|
20
|
+
.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
|
|
21
|
+
.gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
|
|
22
|
+
.tr("\u00a0", ' ')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def [](name)
|
|
26
|
+
native.attribute(name.to_s)
|
|
27
|
+
rescue Selenium::WebDriver::Error::WebDriverError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def value
|
|
32
|
+
if tag_name == 'select' && multiple?
|
|
33
|
+
native.find_elements(:css, 'option:checked').map { |el| el[:value] || el.text }
|
|
34
|
+
else
|
|
35
|
+
native[:value]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def style(styles)
|
|
40
|
+
styles.each_with_object({}) do |style, result|
|
|
41
|
+
result[style] = native.css_value(style)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
#
|
|
47
|
+
# Set the value of the form element to the given value.
|
|
48
|
+
#
|
|
49
|
+
# @param [String] value The new value
|
|
50
|
+
# @param [Hash{}] options Driver specific options for how to set the value
|
|
51
|
+
# @option options [Symbol,Array] :clear (nil) The method used to clear the previous value <br/>
|
|
52
|
+
# nil => clear via javascript <br/>
|
|
53
|
+
# :none => append the new value to the existing value <br/>
|
|
54
|
+
# :backspace => send backspace keystrokes to clear the field <br/>
|
|
55
|
+
# Array => an array of keys to send before the value being set, e.g. [[:command, 'a'], :backspace]
|
|
56
|
+
def set(value, **options)
|
|
57
|
+
if value.is_a?(Array) && !multiple?
|
|
58
|
+
raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
|
|
62
|
+
@tag_name ||= tag_name
|
|
63
|
+
|
|
64
|
+
case tag_name
|
|
65
|
+
when 'input'
|
|
66
|
+
case type
|
|
67
|
+
when 'radio'
|
|
68
|
+
click
|
|
69
|
+
when 'checkbox'
|
|
70
|
+
click if value ^ checked?
|
|
71
|
+
when 'file'
|
|
72
|
+
set_file(value)
|
|
73
|
+
when 'date'
|
|
74
|
+
set_date(value)
|
|
75
|
+
when 'time'
|
|
76
|
+
set_time(value)
|
|
77
|
+
when 'datetime-local'
|
|
78
|
+
set_datetime_local(value)
|
|
79
|
+
when 'color'
|
|
80
|
+
set_color(value)
|
|
81
|
+
when 'range'
|
|
82
|
+
set_range(value)
|
|
83
|
+
else
|
|
84
|
+
set_text(value, **options)
|
|
85
|
+
end
|
|
86
|
+
when 'textarea'
|
|
87
|
+
set_text(value, **options)
|
|
88
|
+
else
|
|
89
|
+
set_content_editable(value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def select_option
|
|
94
|
+
click unless selected? || disabled?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unselect_option
|
|
98
|
+
raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.' unless select_node.multiple?
|
|
99
|
+
|
|
100
|
+
click if selected?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def click(keys = [], **options)
|
|
104
|
+
click_options = ClickOptions.new(keys, options)
|
|
105
|
+
return native.click if click_options.empty?
|
|
106
|
+
|
|
107
|
+
perform_with_options(click_options) do |action|
|
|
108
|
+
target = click_options.coords? ? nil : native
|
|
109
|
+
if click_options.delay.zero?
|
|
110
|
+
action.click(target)
|
|
111
|
+
else
|
|
112
|
+
action.click_and_hold(target)
|
|
113
|
+
if w3c?
|
|
114
|
+
action.pause(action.pointer_inputs.first, click_options.delay)
|
|
115
|
+
else
|
|
116
|
+
action.pause(click_options.delay)
|
|
117
|
+
end
|
|
118
|
+
action.release
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
if e.is_a?(::Selenium::WebDriver::Error::ElementClickInterceptedError) ||
|
|
123
|
+
e.message.match?(/Other element would receive the click/)
|
|
124
|
+
scroll_to_center
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
raise e
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def right_click(keys = [], **options)
|
|
131
|
+
click_options = ClickOptions.new(keys, options)
|
|
132
|
+
perform_with_options(click_options) do |action|
|
|
133
|
+
target = click_options.coords? ? nil : native
|
|
134
|
+
if click_options.delay.zero?
|
|
135
|
+
action.context_click(target)
|
|
136
|
+
elsif w3c?
|
|
137
|
+
action.move_to(target) if target
|
|
138
|
+
action.pointer_down(:right)
|
|
139
|
+
.pause(action.pointer_inputs.first, click_options.delay)
|
|
140
|
+
.pointer_up(:right)
|
|
141
|
+
else
|
|
142
|
+
raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def double_click(keys = [], **options)
|
|
148
|
+
click_options = ClickOptions.new(keys, options)
|
|
149
|
+
raise ArgumentError, "double_click doesn't support a delay option" unless click_options.delay.zero?
|
|
150
|
+
|
|
151
|
+
perform_with_options(click_options) do |action|
|
|
152
|
+
click_options.coords? ? action.double_click : action.double_click(native)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def send_keys(*args)
|
|
157
|
+
native.send_keys(*args)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def hover
|
|
161
|
+
scroll_if_needed { browser_action.move_to(native).perform }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def drag_to(element, drop_modifiers: [], **)
|
|
165
|
+
drop_modifiers = Array(drop_modifiers)
|
|
166
|
+
# Due to W3C spec compliance - The Actions API no longer scrolls to elements when necessary
|
|
167
|
+
# which means Seleniums `drag_and_drop` is now broken - do it manually
|
|
168
|
+
scroll_if_needed { browser_action.click_and_hold(native).perform }
|
|
169
|
+
# element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
|
|
170
|
+
element.scroll_if_needed do
|
|
171
|
+
keys_down = modifiers_down(browser_action, drop_modifiers)
|
|
172
|
+
keys_up = modifiers_up(keys_down.move_to(element.native).release, drop_modifiers)
|
|
173
|
+
keys_up.perform
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def drop(*_)
|
|
178
|
+
raise NotImplementedError, 'Out of browser drop emulation is not implemented for the current browser'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def tag_name
|
|
182
|
+
@tag_name ||= native.tag_name.downcase
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def visible?; boolean_attr(native.displayed?); end
|
|
186
|
+
def readonly?; boolean_attr(self[:readonly]); end
|
|
187
|
+
def multiple?; boolean_attr(self[:multiple]); end
|
|
188
|
+
def selected?; boolean_attr(native.selected?); end
|
|
189
|
+
alias :checked? :selected?
|
|
190
|
+
|
|
191
|
+
def disabled?
|
|
192
|
+
return true unless native.enabled?
|
|
193
|
+
|
|
194
|
+
# WebDriver only defines `disabled?` for form controls but fieldset makes sense too
|
|
195
|
+
find_xpath('self::fieldset/ancestor-or-self::fieldset[@disabled]').any?
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def content_editable?
|
|
199
|
+
native.attribute('isContentEditable') == 'true'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ==(other)
|
|
203
|
+
native == other.native
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def path
|
|
207
|
+
driver.evaluate_script GET_XPATH_SCRIPT, self
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def obscured?(x: nil, y: nil)
|
|
211
|
+
res = driver.evaluate_script(OBSCURED_OR_OFFSET_SCRIPT, self, x, y)
|
|
212
|
+
return true if res == true
|
|
213
|
+
|
|
214
|
+
driver.frame_obscured_at?(x: res['x'], y: res['y'])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def rect
|
|
218
|
+
native.rect
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
protected
|
|
222
|
+
|
|
223
|
+
def scroll_if_needed
|
|
224
|
+
yield
|
|
225
|
+
rescue ::Selenium::WebDriver::Error::MoveTargetOutOfBoundsError
|
|
226
|
+
scroll_to_center
|
|
227
|
+
yield
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def scroll_to_center
|
|
231
|
+
script = <<-'JS'
|
|
232
|
+
try {
|
|
233
|
+
arguments[0].scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
|
234
|
+
} catch(e) {
|
|
235
|
+
arguments[0].scrollIntoView(true);
|
|
236
|
+
}
|
|
237
|
+
JS
|
|
238
|
+
begin
|
|
239
|
+
driver.execute_script(script, self)
|
|
240
|
+
rescue StandardError
|
|
241
|
+
# Swallow error if scrollIntoView with options isn't supported
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
def sibling_index(parent, node, selector)
|
|
248
|
+
siblings = parent.find_xpath(selector)
|
|
249
|
+
case siblings.size
|
|
250
|
+
when 0
|
|
251
|
+
'[ERROR]' # IE doesn't support full XPath (namespace-uri, etc)
|
|
252
|
+
when 1
|
|
253
|
+
'' # index not necessary when only one matching element
|
|
254
|
+
else
|
|
255
|
+
idx = siblings.index(node)
|
|
256
|
+
# Element may not be found in the siblings if it has gone away
|
|
257
|
+
idx.nil? ? '[ERROR]' : "[#{idx + 1}]"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def boolean_attr(val)
|
|
262
|
+
val && (val != 'false')
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# a reference to the select node if this is an option node
|
|
266
|
+
def select_node
|
|
267
|
+
find_xpath(XPath.ancestor(:select)[1]).first
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def set_text(value, clear: nil, rapid: nil, **_unused)
|
|
271
|
+
value = value.to_s
|
|
272
|
+
if value.empty? && clear.nil?
|
|
273
|
+
native.clear
|
|
274
|
+
elsif clear == :backspace
|
|
275
|
+
# Clear field by sending the correct number of backspace keys.
|
|
276
|
+
backspaces = [:backspace] * self.value.to_s.length
|
|
277
|
+
send_keys(*([:end] + backspaces + [value]))
|
|
278
|
+
elsif clear.is_a? Array
|
|
279
|
+
send_keys(*clear, value)
|
|
280
|
+
else
|
|
281
|
+
driver.execute_script 'arguments[0].select()', self unless clear == :none
|
|
282
|
+
if rapid == true || ((value.length > auto_rapid_set_length) && rapid != false)
|
|
283
|
+
send_keys(value[0..3])
|
|
284
|
+
driver.execute_script RAPID_SET_TEXT, self, value[0...-3]
|
|
285
|
+
send_keys(value[-3..-1])
|
|
286
|
+
else
|
|
287
|
+
send_keys(value)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def auto_rapid_set_length
|
|
293
|
+
30
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def perform_with_options(click_options, &block)
|
|
297
|
+
raise ArgumentError, 'A block must be provided' unless block
|
|
298
|
+
|
|
299
|
+
scroll_if_needed do
|
|
300
|
+
action_with_modifiers(click_options) do |action|
|
|
301
|
+
if block_given?
|
|
302
|
+
yield action
|
|
303
|
+
else
|
|
304
|
+
click_options.coords? ? action.click : action.click(native)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def set_date(value) # rubocop:disable Naming/AccessorMethodName
|
|
311
|
+
value = SettableValue.new(value)
|
|
312
|
+
return set_text(value) unless value.dateable?
|
|
313
|
+
|
|
314
|
+
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
315
|
+
update_value_js(value.to_date_str)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def set_time(value) # rubocop:disable Naming/AccessorMethodName
|
|
319
|
+
value = SettableValue.new(value)
|
|
320
|
+
return set_text(value) unless value.timeable?
|
|
321
|
+
|
|
322
|
+
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
323
|
+
update_value_js(value.to_time_str)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def set_datetime_local(value) # rubocop:disable Naming/AccessorMethodName
|
|
327
|
+
value = SettableValue.new(value)
|
|
328
|
+
return set_text(value) unless value.timeable?
|
|
329
|
+
|
|
330
|
+
# TODO: this would be better if locale can be detected and correct keystrokes sent
|
|
331
|
+
update_value_js(value.to_datetime_str)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def set_color(value) # rubocop:disable Naming/AccessorMethodName
|
|
335
|
+
update_value_js(value)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def set_range(value) # rubocop:disable Naming/AccessorMethodName
|
|
339
|
+
update_value_js(value)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def update_value_js(value)
|
|
343
|
+
driver.execute_script(<<-JS, self, value)
|
|
344
|
+
if (arguments[0].readOnly) { return };
|
|
345
|
+
if (document.activeElement !== arguments[0]){
|
|
346
|
+
arguments[0].focus();
|
|
347
|
+
}
|
|
348
|
+
if (arguments[0].value != arguments[1]) {
|
|
349
|
+
arguments[0].value = arguments[1]
|
|
350
|
+
arguments[0].dispatchEvent(new InputEvent('input'));
|
|
351
|
+
arguments[0].dispatchEvent(new Event('change', { bubbles: true }));
|
|
352
|
+
}
|
|
353
|
+
JS
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def set_file(value) # rubocop:disable Naming/AccessorMethodName
|
|
357
|
+
with_file_detector do
|
|
358
|
+
path_names = value.to_s.empty? ? [] : value
|
|
359
|
+
file_names = Array(path_names).map do |pn|
|
|
360
|
+
Pathname.new(pn).absolute? ? pn : File.expand_path(pn)
|
|
361
|
+
end.join("\n")
|
|
362
|
+
native.send_keys(file_names)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def with_file_detector
|
|
367
|
+
if driver.options[:browser] == :remote &&
|
|
368
|
+
bridge.respond_to?(:file_detector) &&
|
|
369
|
+
bridge.file_detector.nil?
|
|
370
|
+
begin
|
|
371
|
+
bridge.file_detector = lambda do |(fn, *)|
|
|
372
|
+
str = fn.to_s
|
|
373
|
+
str if File.exist?(str)
|
|
374
|
+
end
|
|
375
|
+
yield
|
|
376
|
+
ensure
|
|
377
|
+
bridge.file_detector = nil
|
|
378
|
+
end
|
|
379
|
+
else
|
|
380
|
+
yield
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def set_content_editable(value) # rubocop:disable Naming/AccessorMethodName
|
|
385
|
+
# Ensure we are focused on the element
|
|
386
|
+
click
|
|
387
|
+
|
|
388
|
+
editable = driver.execute_script <<-JS, self
|
|
389
|
+
if (arguments[0].isContentEditable) {
|
|
390
|
+
var range = document.createRange();
|
|
391
|
+
var sel = window.getSelection();
|
|
392
|
+
arguments[0].focus();
|
|
393
|
+
range.selectNodeContents(arguments[0]);
|
|
394
|
+
sel.removeAllRanges();
|
|
395
|
+
sel.addRange(range);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
JS
|
|
400
|
+
|
|
401
|
+
# The action api has a speed problem but both chrome and firefox 58 raise errors
|
|
402
|
+
# if we use the faster direct send_keys. For now just send_keys to the element
|
|
403
|
+
# we've already focused.
|
|
404
|
+
# native.send_keys(value.to_s)
|
|
405
|
+
browser_action.send_keys(value.to_s).perform if editable
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def action_with_modifiers(click_options)
|
|
409
|
+
actions = browser_action.tap do |acts|
|
|
410
|
+
if click_options.center_offset? && click_options.coords?
|
|
411
|
+
acts.move_to(native).move_by(*click_options.coords)
|
|
412
|
+
else
|
|
413
|
+
acts.move_to(native, *click_options.coords)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
modifiers_down(actions, click_options.keys)
|
|
417
|
+
yield actions
|
|
418
|
+
modifiers_up(actions, click_options.keys)
|
|
419
|
+
actions.perform
|
|
420
|
+
ensure
|
|
421
|
+
act = browser_action
|
|
422
|
+
act.release_actions if act.respond_to?(:release_actions)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def modifiers_down(actions, keys)
|
|
426
|
+
each_key(keys) { |key| actions.key_down(key) }
|
|
427
|
+
actions
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def modifiers_up(actions, keys)
|
|
431
|
+
each_key(keys) { |key| actions.key_up(key) }
|
|
432
|
+
actions
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def browser
|
|
436
|
+
driver.browser
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def bridge
|
|
440
|
+
browser.send(:bridge)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def browser_action
|
|
444
|
+
browser.action
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def capabilities
|
|
448
|
+
browser.capabilities
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def w3c?
|
|
452
|
+
(defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
|
|
453
|
+
capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def normalize_keys(keys)
|
|
457
|
+
keys.map do |key|
|
|
458
|
+
case key
|
|
459
|
+
when :ctrl then :control
|
|
460
|
+
when :command, :cmd then :meta
|
|
461
|
+
else
|
|
462
|
+
key
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def each_key(keys)
|
|
468
|
+
normalize_keys(keys).each { |key| yield(key) }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def find_context
|
|
472
|
+
native
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def build_node(native_node, initial_cache = {})
|
|
476
|
+
self.class.new(driver, native_node, initial_cache)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def attrs(*attr_names)
|
|
480
|
+
return attr_names.map { |name| self[name.to_s] } if ENV['CAPYBARA_THOROUGH']
|
|
481
|
+
|
|
482
|
+
driver.evaluate_script <<~'JS', self, attr_names.map(&:to_s)
|
|
483
|
+
(function(el, names){
|
|
484
|
+
return names.map(function(name){
|
|
485
|
+
return el[name]
|
|
486
|
+
});
|
|
487
|
+
})(arguments[0], arguments[1]);
|
|
488
|
+
JS
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
GET_XPATH_SCRIPT = <<~'JS'
|
|
492
|
+
(function(el, xml){
|
|
493
|
+
var xpath = '';
|
|
494
|
+
var pos, tempitem2;
|
|
495
|
+
|
|
496
|
+
while(el !== xml.documentElement) {
|
|
497
|
+
pos = 0;
|
|
498
|
+
tempitem2 = el;
|
|
499
|
+
while(tempitem2) {
|
|
500
|
+
if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
|
|
501
|
+
pos += 1;
|
|
502
|
+
}
|
|
503
|
+
tempitem2 = tempitem2.previousSibling;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (el.namespaceURI != xml.documentElement.namespaceURI) {
|
|
507
|
+
xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
|
|
508
|
+
} else {
|
|
509
|
+
xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
el = el.parentNode;
|
|
513
|
+
}
|
|
514
|
+
xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
|
|
515
|
+
xpath = xpath.replace(/\/$/, '');
|
|
516
|
+
return xpath;
|
|
517
|
+
})(arguments[0], document)
|
|
518
|
+
JS
|
|
519
|
+
|
|
520
|
+
OBSCURED_OR_OFFSET_SCRIPT = <<~'JS'
|
|
521
|
+
(function(el, x, y) {
|
|
522
|
+
var box = el.getBoundingClientRect();
|
|
523
|
+
if (x == null) x = box.width/2;
|
|
524
|
+
if (y == null) y = box.height/2 ;
|
|
525
|
+
|
|
526
|
+
var px = box.left + x,
|
|
527
|
+
py = box.top + y,
|
|
528
|
+
e = document.elementFromPoint(px, py);
|
|
529
|
+
|
|
530
|
+
if (!el.contains(e))
|
|
531
|
+
return true;
|
|
532
|
+
|
|
533
|
+
return { x: px, y: py };
|
|
534
|
+
})(arguments[0], arguments[1], arguments[2])
|
|
535
|
+
JS
|
|
536
|
+
|
|
537
|
+
RAPID_SET_TEXT = <<~'JS'
|
|
538
|
+
(function(el, value) {
|
|
539
|
+
if (el.maxLength && el.maxLength != -1){
|
|
540
|
+
value = value.slice(0, el.maxLength);
|
|
541
|
+
}
|
|
542
|
+
el.value = value;
|
|
543
|
+
})(arguments[0], arguments[1])
|
|
544
|
+
JS
|
|
545
|
+
|
|
546
|
+
# SettableValue encapsulates time/date field formatting
|
|
547
|
+
class SettableValue
|
|
548
|
+
attr_reader :value
|
|
549
|
+
|
|
550
|
+
def initialize(value)
|
|
551
|
+
@value = value
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def to_s
|
|
555
|
+
value.to_s
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def dateable?
|
|
559
|
+
!value.is_a?(String) && value.respond_to?(:to_date)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def to_date_str
|
|
563
|
+
value.to_date.iso8601
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def timeable?
|
|
567
|
+
!value.is_a?(String) && value.respond_to?(:to_time)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def to_time_str
|
|
571
|
+
value.to_time.strftime('%H:%M')
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def to_datetime_str
|
|
575
|
+
value.to_time.strftime('%Y-%m-%dT%H:%M')
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
private_constant :SettableValue
|
|
579
|
+
|
|
580
|
+
# ClickOptions encapsulates click option logic
|
|
581
|
+
class ClickOptions
|
|
582
|
+
attr_reader :keys, :options
|
|
583
|
+
|
|
584
|
+
def initialize(keys, options)
|
|
585
|
+
@keys = keys
|
|
586
|
+
@options = options
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def coords?
|
|
590
|
+
options[:x] && options[:y]
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def coords
|
|
594
|
+
[options[:x], options[:y]]
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def center_offset?
|
|
598
|
+
options[:offset] == :center
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def empty?
|
|
602
|
+
keys.empty? && !coords? && delay.zero?
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def delay
|
|
606
|
+
options[:delay] || 0
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
private_constant :ClickOptions
|
|
610
|
+
end
|