capybara 3.23.0 → 3.35.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +264 -11
  3. data/README.md +10 -6
  4. data/lib/capybara.rb +20 -8
  5. data/lib/capybara/config.rb +10 -8
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +4 -0
  8. data/lib/capybara/driver/node.rb +4 -0
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +28 -2
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/minitest/spec.rb +156 -97
  13. data/lib/capybara/node/actions.rb +36 -36
  14. data/lib/capybara/node/base.rb +6 -6
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +77 -33
  18. data/lib/capybara/node/finders.rb +24 -17
  19. data/lib/capybara/node/matchers.rb +79 -64
  20. data/lib/capybara/node/simple.rb +11 -4
  21. data/lib/capybara/queries/ancestor_query.rb +6 -10
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +259 -23
  25. data/lib/capybara/queries/sibling_query.rb +5 -11
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/rack_test/browser.rb +13 -4
  29. data/lib/capybara/rack_test/driver.rb +2 -1
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +42 -6
  32. data/lib/capybara/registration_container.rb +44 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  35. data/lib/capybara/registrations/servers.rb +9 -2
  36. data/lib/capybara/result.rb +39 -19
  37. data/lib/capybara/rspec.rb +2 -0
  38. data/lib/capybara/rspec/matcher_proxies.rb +5 -5
  39. data/lib/capybara/rspec/matchers.rb +97 -74
  40. data/lib/capybara/rspec/matchers/base.rb +19 -6
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +5 -7
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +15 -10
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +4 -7
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -7
  47. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  48. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  49. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +46 -19
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  54. data/lib/capybara/selector/css.rb +1 -1
  55. data/lib/capybara/selector/definition.rb +13 -11
  56. data/lib/capybara/selector/definition/button.rb +32 -15
  57. data/lib/capybara/selector/definition/checkbox.rb +2 -2
  58. data/lib/capybara/selector/definition/css.rb +3 -1
  59. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  60. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  61. data/lib/capybara/selector/definition/element.rb +3 -2
  62. data/lib/capybara/selector/definition/field.rb +1 -1
  63. data/lib/capybara/selector/definition/file_field.rb +1 -1
  64. data/lib/capybara/selector/definition/fillable_field.rb +2 -2
  65. data/lib/capybara/selector/definition/label.rb +5 -3
  66. data/lib/capybara/selector/definition/link.rb +8 -0
  67. data/lib/capybara/selector/definition/option.rb +1 -1
  68. data/lib/capybara/selector/definition/radio_button.rb +2 -2
  69. data/lib/capybara/selector/definition/select.rb +33 -14
  70. data/lib/capybara/selector/definition/table.rb +6 -3
  71. data/lib/capybara/selector/definition/table_row.rb +2 -2
  72. data/lib/capybara/selector/filter_set.rb +13 -11
  73. data/lib/capybara/selector/filters/base.rb +6 -1
  74. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  75. data/lib/capybara/selector/regexp_disassembler.rb +7 -0
  76. data/lib/capybara/selector/selector.rb +13 -3
  77. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  78. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -1
  79. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  80. data/lib/capybara/selenium/atoms/src/isDisplayed.js +10 -10
  81. data/lib/capybara/selenium/driver.rb +86 -24
  82. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +24 -21
  83. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +21 -19
  84. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +17 -1
  85. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +0 -4
  86. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  87. data/lib/capybara/selenium/extensions/find.rb +37 -26
  88. data/lib/capybara/selenium/extensions/html5_drag.rb +55 -11
  89. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  90. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  91. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  92. data/lib/capybara/selenium/node.rb +160 -40
  93. data/lib/capybara/selenium/nodes/chrome_node.rb +72 -12
  94. data/lib/capybara/selenium/nodes/edge_node.rb +32 -14
  95. data/lib/capybara/selenium/nodes/firefox_node.rb +28 -32
  96. data/lib/capybara/selenium/nodes/safari_node.rb +5 -29
  97. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  98. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  99. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  100. data/lib/capybara/selenium/patches/logs.rb +32 -7
  101. data/lib/capybara/server.rb +19 -3
  102. data/lib/capybara/server/animation_disabler.rb +8 -3
  103. data/lib/capybara/server/checker.rb +1 -1
  104. data/lib/capybara/server/middleware.rb +22 -10
  105. data/lib/capybara/session.rb +66 -40
  106. data/lib/capybara/session/config.rb +11 -3
  107. data/lib/capybara/session/matchers.rb +11 -11
  108. data/lib/capybara/spec/public/offset.js +6 -0
  109. data/lib/capybara/spec/public/test.js +75 -7
  110. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  111. data/lib/capybara/spec/session/all_spec.rb +60 -5
  112. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  113. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  114. data/lib/capybara/spec/session/check_spec.rb +6 -0
  115. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  116. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  117. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  118. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  119. data/lib/capybara/spec/session/find_spec.rb +55 -0
  120. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -0
  121. data/lib/capybara/spec/session/has_button_spec.rb +51 -0
  122. data/lib/capybara/spec/session/has_css_spec.rb +26 -4
  123. data/lib/capybara/spec/session/has_current_path_spec.rb +15 -2
  124. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  125. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  126. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  127. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  128. data/lib/capybara/spec/session/has_text_spec.rb +30 -0
  129. data/lib/capybara/spec/session/html_spec.rb +1 -1
  130. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  131. data/lib/capybara/spec/session/node_spec.rb +394 -9
  132. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  133. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  134. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  135. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -15
  136. data/lib/capybara/spec/session/selectors_spec.rb +16 -3
  137. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  138. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  139. data/lib/capybara/spec/session/window/window_spec.rb +8 -8
  140. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  141. data/lib/capybara/spec/spec_helper.rb +14 -14
  142. data/lib/capybara/spec/test_app.rb +27 -21
  143. data/lib/capybara/spec/views/form.erb +47 -4
  144. data/lib/capybara/spec/views/offset.erb +32 -0
  145. data/lib/capybara/spec/views/spatial.erb +31 -0
  146. data/lib/capybara/spec/views/with_animation.erb +37 -1
  147. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  148. data/lib/capybara/spec/views/with_html.erb +24 -2
  149. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  150. data/lib/capybara/spec/views/with_js.erb +4 -1
  151. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  152. data/lib/capybara/spec/views/with_sortable_js.erb +1 -1
  153. data/lib/capybara/version.rb +1 -1
  154. data/lib/capybara/window.rb +3 -7
  155. data/spec/basic_node_spec.rb +15 -14
  156. data/spec/capybara_spec.rb +28 -28
  157. data/spec/dsl_spec.rb +16 -3
  158. data/spec/filter_set_spec.rb +5 -5
  159. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  160. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  161. data/spec/minitest_spec.rb +3 -2
  162. data/spec/minitest_spec_spec.rb +46 -46
  163. data/spec/rack_test_spec.rb +38 -15
  164. data/spec/regexp_dissassembler_spec.rb +52 -38
  165. data/spec/result_spec.rb +43 -32
  166. data/spec/rspec/features_spec.rb +4 -1
  167. data/spec/rspec/scenarios_spec.rb +4 -0
  168. data/spec/rspec/shared_spec_matchers.rb +68 -56
  169. data/spec/rspec_spec.rb +9 -5
  170. data/spec/selector_spec.rb +32 -17
  171. data/spec/selenium_spec_chrome.rb +78 -11
  172. data/spec/selenium_spec_chrome_remote.rb +23 -6
  173. data/spec/selenium_spec_edge.rb +15 -12
  174. data/spec/selenium_spec_firefox.rb +24 -19
  175. data/spec/selenium_spec_firefox_remote.rb +0 -8
  176. data/spec/selenium_spec_ie.rb +1 -6
  177. data/spec/server_spec.rb +106 -44
  178. data/spec/session_spec.rb +5 -5
  179. data/spec/shared_selenium_node.rb +56 -2
  180. data/spec/shared_selenium_session.rb +122 -15
  181. data/spec/spec_helper.rb +2 -2
  182. metadata +63 -17
  183. data/lib/capybara/spec/session/source_spec.rb +0 -0
@@ -3,25 +3,29 @@
3
3
  require 'capybara/selenium/nodes/edge_node'
4
4
 
5
5
  module Capybara::Selenium::Driver::EdgeDriver
6
+ def self.extended(base)
7
+ bridge = base.send(:bridge)
8
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
9
+ base.options[:native_displayed] = false if base.options[:native_displayed].nil?
10
+ end
11
+
6
12
  def fullscreen_window(handle)
7
13
  return super if edgedriver_version < 75
8
14
 
9
15
  within_given_window(handle) do
10
- begin
11
- super
12
- rescue NoMethodError => e
13
- raise unless e.message.match?(/full_screen_window/)
14
-
15
- result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
16
- result['value']
17
- end
16
+ super
17
+ rescue NoMethodError => e
18
+ raise unless e.message.include?('full_screen_window')
19
+
20
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {})
21
+ result['value']
18
22
  end
19
23
  end
20
24
 
21
25
  def resize_window_to(handle, width, height)
22
26
  super
23
27
  rescue Selenium::WebDriver::Error::UnknownError => e
24
- raise unless e.message.match?(/failed to change window state/)
28
+ raise unless e.message.include?('failed to change window state')
25
29
 
26
30
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
27
31
  # and raises unnecessary error. Wait a bit and try again.
@@ -39,8 +43,8 @@ module Capybara::Selenium::Driver::EdgeDriver
39
43
 
40
44
  timer = Capybara::Helpers.timer(expire_in: 10)
41
45
  begin
42
- @browser.navigate.to('about:blank')
43
46
  clear_storage unless uniform_storage_clear?
47
+ @browser.navigate.to('about:blank')
44
48
  wait_for_empty_page(timer)
45
49
  rescue *unhandled_alert_errors
46
50
  accept_unhandled_reset_alert
@@ -68,17 +72,19 @@ private
68
72
  end
69
73
 
70
74
  def clear_all_storage?
71
- options.values_at(:clear_session_storage, :clear_local_storage).none? { |s| s == false }
75
+ storage_clears.none? false
72
76
  end
73
77
 
74
78
  def uniform_storage_clear?
75
- clear = options.values_at(:clear_session_storage, :clear_local_storage)
76
- clear.all? { |s| s == false } || clear.none? { |s| s == false }
79
+ storage_clears.uniq { |s| s == false }.length <= 1
80
+ end
81
+
82
+ def storage_clears
83
+ options.values_at(:clear_session_storage, :clear_local_storage)
77
84
  end
78
85
 
79
86
  def clear_storage
80
- # Chrome errors if attempt to clear storage on about:blank
81
- # In W3C mode it crashes chromedriver
87
+ # Edgedriver crashes if attempt to clear storage on about:blank
82
88
  url = current_url
83
89
  super unless url.nil? || url.start_with?('about:')
84
90
  end
@@ -106,10 +112,6 @@ private
106
112
  ::Capybara::Selenium::EdgeNode.new(self, native_node, initial_cache)
107
113
  end
108
114
 
109
- def bridge
110
- browser.send(:bridge)
111
- end
112
-
113
115
  def edgedriver_version
114
116
  @edgedriver_version ||= begin
115
117
  caps = browser.capabilities
@@ -5,6 +5,8 @@ require 'capybara/selenium/nodes/firefox_node'
5
5
  module Capybara::Selenium::Driver::FirefoxDriver
6
6
  def self.extended(driver)
7
7
  driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver if w3c?(driver)
8
+ bridge = driver.send(:bridge)
9
+ bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
8
10
  end
9
11
 
10
12
  def self.w3c?(driver)
@@ -17,6 +19,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
17
19
  class << self
18
20
  def extended(driver)
19
21
  require 'capybara/selenium/patches/pause_duration_fix' if pause_broken?(driver.browser)
22
+ driver.options[:native_displayed] = false if driver.options[:native_displayed].nil?
20
23
  end
21
24
 
22
25
  def pause_broken?(sel_driver)
@@ -39,6 +42,15 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
39
42
  # Use instance variable directly so we avoid starting the browser just to reset the session
40
43
  return unless @browser
41
44
 
45
+ if browser_version >= 68
46
+ begin
47
+ # Firefox 68 hangs if we try to switch windows while a modal is visible
48
+ browser.switch_to.alert&.dismiss
49
+ rescue Selenium::WebDriver::Error::NoSuchAlertError
50
+ # Swallow
51
+ end
52
+ end
53
+
42
54
  switch_to_window(window_handles.first)
43
55
  window_handles.slice(1..-1).each { |win| close_window(win) }
44
56
  super
@@ -49,7 +61,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
49
61
  accept_modal :confirm, wait: 0.1 do
50
62
  super
51
63
  end
52
- rescue Capybara::ModalNotFound # rubocop:disable Lint/HandleExceptions
64
+ rescue Capybara::ModalNotFound
53
65
  # No modal was opened - page has refreshed - ignore
54
66
  end
55
67
 
@@ -68,6 +80,10 @@ private
68
80
  def build_node(native_node, initial_cache = {})
69
81
  ::Capybara::Selenium::FirefoxNode.new(self, native_node, initial_cache)
70
82
  end
83
+
84
+ def browser_version
85
+ browser.capabilities[:browser_version].to_f
86
+ end
71
87
  end
72
88
 
73
89
  Capybara::Selenium::Driver.register_specialization :firefox, Capybara::Selenium::Driver::FirefoxDriver
@@ -18,10 +18,6 @@ private
18
18
  def build_node(native_node, initial_cache = {})
19
19
  ::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
20
20
  end
21
-
22
- def bridge
23
- browser.send(:bridge)
24
- end
25
21
  end
26
22
 
27
23
  Capybara::Selenium::Driver.register_specialization(/^(safari|Safari_Technology_Preview)$/,
@@ -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,42 +3,44 @@
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
- begin
22
- els = filter_by_text(els, texts) unless texts.empty?
23
- hints_js, functions = build_hints_js(uses_visibility, styles)
24
-
25
- unless functions.empty?
26
- hints = es_context.execute_script(hints_js, els).map! do |results|
27
- hint = {}
28
- hint[:style] = results.pop if functions.include?(:style_func)
29
- hint[:visible] = results.pop if functions.include?(:vis_func)
30
- hint
31
- end
32
- end
33
- rescue ::Selenium::WebDriver::Error::StaleElementReferenceError,
34
- ::Capybara::NotSupportedByDriverError
35
- # warn 'Unexpected Stale Element Error - skipping optimization'
36
- hints = []
37
- end
21
+ els = filter_by_text(els, texts) unless texts.empty?
22
+ hints = gather_hints(els, uses_visibility: uses_visibility, styles: styles, position: position)
38
23
  end
39
24
  els.map.with_index { |el, idx| build_node(el, hints[idx] || {}) }
40
25
  end
41
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
+
42
44
  def filter_by_text(elements, texts)
43
45
  es_context.execute_script <<~JS, elements, texts
44
46
  var texts = arguments[1];
@@ -49,7 +51,7 @@ module Capybara
49
51
  JS
50
52
  end
51
53
 
52
- def build_hints_js(uses_visibility, styles)
54
+ def build_hints_js(uses_visibility, styles, position)
53
55
  functions = []
54
56
  hints_js = +''
55
57
 
@@ -60,6 +62,15 @@ module Capybara
60
62
  functions << :vis_func
61
63
  end
62
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
+
63
74
  if styles.is_a? Hash
64
75
  hints_js << <<~STYLE_JS
65
76
  var style_func = function(el){
@@ -89,9 +100,9 @@ module Capybara
89
100
  def is_displayed_atom # rubocop:disable Naming/PredicateName
90
101
  @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
91
102
  browser.send(:bridge).send(:read_atom, 'isDisplayed')
92
- rescue StandardError
93
- # If the atom doesn't exist or other error
94
- ''
103
+ rescue StandardError
104
+ # If the atom doesn't exist or other error
105
+ ''
95
106
  end
96
107
  end
97
108
  end
@@ -4,19 +4,35 @@ class Capybara::Selenium::Node
4
4
  module Html5Drag
5
5
  # Implement methods to emulate HTML5 drag and drop
6
6
 
7
- def drag_to(element, delay: 0.05)
7
+ def drag_to(element, html5: nil, delay: 0.05, drop_modifiers: [])
8
+ drop_modifiers = Array(drop_modifiers)
9
+
8
10
  driver.execute_script MOUSEDOWN_TRACKER
9
11
  scroll_if_needed { browser_action.click_and_hold(native).perform }
10
- if driver.evaluate_script('window.capybara_mousedown_prevented || !arguments[0].draggable', self)
11
- element.scroll_if_needed { browser_action.move_to(element.native).release.perform }
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
- driver.evaluate_async_script HTML5_DRAG_DROP_SCRIPT, self, element, delay * 1000
14
- browser_action.release.perform
16
+ perform_legacy_drag(element, drop_modifiers)
15
17
  end
16
18
  end
17
19
 
18
20
  private
19
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
+
20
36
  def html5_drop(*args)
21
37
  if args[0].is_a? String
22
38
  input = driver.evaluate_script ATTACH_FILE
@@ -81,11 +97,25 @@ class Capybara::Selenium::Node
81
97
  JS
82
98
 
83
99
  MOUSEDOWN_TRACKER = <<~JS
100
+ window.capybara_mousedown_prevented = null;
84
101
  document.addEventListener('mousedown', ev => {
85
102
  window.capybara_mousedown_prevented = ev.defaultPrevented;
86
103
  }, { once: true, passive: true })
87
104
  JS
88
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
+
89
119
  HTML5_DRAG_DROP_SCRIPT = <<~JS
90
120
  function rectCenter(rect){
91
121
  return new DOMPoint(
@@ -130,6 +160,14 @@ class Capybara::Selenium::Node
130
160
  var targetRect = target.getBoundingClientRect();
131
161
  var sourceCenter = rectCenter(source.getBoundingClientRect());
132
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
+
133
171
  // fire 2 dragover events to simulate dragging with a direction
134
172
  var entryPoint = pointOnRect(sourceCenter, targetRect)
135
173
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
@@ -143,17 +181,18 @@ class Capybara::Selenium::Node
143
181
  var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
144
182
  var dragOverEvent = new DragEvent('dragover', dragOverOpts);
145
183
  target.dispatchEvent(dragOverEvent);
146
- window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented);
184
+ window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
147
185
  }
148
186
 
149
- function dragLeave(drop) {
150
- var dragLeaveEvent = new DragEvent('dragleave', opts);
187
+ function dragLeave(drop, dragOverOpts) {
188
+ var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
189
+ var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
151
190
  target.dispatchEvent(dragLeaveEvent);
152
191
  if (drop) {
153
- var dropEvent = new DragEvent('drop', opts);
192
+ var dropEvent = new DragEvent('drop', dragLeaveOptions);
154
193
  target.dispatchEvent(dropEvent);
155
194
  }
156
- var dragEndEvent = new DragEvent('dragend', opts);
195
+ var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
157
196
  source.dispatchEvent(dragEndEvent);
158
197
  callback.call(true);
159
198
  }
@@ -161,11 +200,16 @@ class Capybara::Selenium::Node
161
200
  var source = arguments[0],
162
201
  target = arguments[1],
163
202
  step_delay = arguments[2],
164
- callback = arguments[3];
203
+ drop_modifier_keys = arguments[3],
204
+ callback = arguments[4];
165
205
 
166
206
  var dt = new DataTransfer();
167
207
  var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
168
208
 
209
+ while (source && !source.draggable) {
210
+ source = source.parentElement;
211
+ }
212
+
169
213
  if (source.tagName == 'A'){
170
214
  dt.setData('text/uri-list', source.href);
171
215
  dt.setData('text', source.href);
@@ -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, #{scroll_y});
57
+ arguments[0].scrollTo(0, #{SCROLL_POSITIONS[location]});
60
58
  } else {
61
- arguments[0].scrollTop = #{scroll_y};
59
+ arguments[0].scrollTop = #{SCROLL_POSITIONS[location]};
62
60
  }
63
61
  JS
64
62
  end