capybara 3.16.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +321 -0
  4. data/README.md +51 -60
  5. data/lib/capybara.rb +71 -114
  6. data/lib/capybara/config.rb +8 -5
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/node.rb +15 -3
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +5 -3
  11. data/lib/capybara/minitest.rb +242 -141
  12. data/lib/capybara/minitest/spec.rb +159 -90
  13. data/lib/capybara/node/actions.rb +85 -74
  14. data/lib/capybara/node/base.rb +4 -4
  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 +216 -117
  18. data/lib/capybara/node/finders.rb +65 -65
  19. data/lib/capybara/node/matchers.rb +228 -126
  20. data/lib/capybara/node/simple.rb +9 -4
  21. data/lib/capybara/queries/ancestor_query.rb +5 -7
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +1 -1
  24. data/lib/capybara/queries/selector_query.rb +296 -30
  25. data/lib/capybara/queries/sibling_query.rb +5 -4
  26. data/lib/capybara/queries/style_query.rb +2 -2
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/queries/title_query.rb +1 -1
  29. data/lib/capybara/rack_test/browser.rb +7 -2
  30. data/lib/capybara/rack_test/driver.rb +1 -1
  31. data/lib/capybara/rack_test/form.rb +1 -1
  32. data/lib/capybara/rack_test/node.rb +43 -7
  33. data/lib/capybara/registration_container.rb +44 -0
  34. data/lib/capybara/registrations/drivers.rb +36 -0
  35. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  36. data/lib/capybara/registrations/servers.rb +44 -0
  37. data/lib/capybara/result.rb +36 -8
  38. data/lib/capybara/rspec/matcher_proxies.rb +6 -4
  39. data/lib/capybara/rspec/matchers.rb +100 -63
  40. data/lib/capybara/rspec/matchers/base.rb +23 -10
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  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 +2 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +219 -588
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  54. data/lib/capybara/selector/css.rb +4 -2
  55. data/lib/capybara/selector/definition.rb +277 -0
  56. data/lib/capybara/selector/definition/button.rb +52 -0
  57. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  58. data/lib/capybara/selector/definition/css.rb +10 -0
  59. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  60. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  61. data/lib/capybara/selector/definition/element.rb +27 -0
  62. data/lib/capybara/selector/definition/field.rb +40 -0
  63. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  64. data/lib/capybara/selector/definition/file_field.rb +13 -0
  65. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  66. data/lib/capybara/selector/definition/frame.rb +17 -0
  67. data/lib/capybara/selector/definition/id.rb +6 -0
  68. data/lib/capybara/selector/definition/label.rb +62 -0
  69. data/lib/capybara/selector/definition/link.rb +54 -0
  70. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  71. data/lib/capybara/selector/definition/option.rb +27 -0
  72. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  73. data/lib/capybara/selector/definition/select.rb +81 -0
  74. data/lib/capybara/selector/definition/table.rb +109 -0
  75. data/lib/capybara/selector/definition/table_row.rb +21 -0
  76. data/lib/capybara/selector/definition/xpath.rb +5 -0
  77. data/lib/capybara/selector/filter_set.rb +13 -9
  78. data/lib/capybara/selector/filters/base.rb +11 -2
  79. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  80. data/lib/capybara/selector/regexp_disassembler.rb +9 -2
  81. data/lib/capybara/selector/selector.rb +43 -448
  82. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  83. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  84. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  85. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  86. data/lib/capybara/selenium/driver.rb +125 -56
  87. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +73 -17
  88. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  89. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +41 -2
  90. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  91. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -5
  92. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  93. data/lib/capybara/selenium/extensions/find.rb +67 -45
  94. data/lib/capybara/selenium/extensions/html5_drag.rb +152 -36
  95. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  96. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  97. data/lib/capybara/selenium/node.rb +227 -56
  98. data/lib/capybara/selenium/nodes/chrome_node.rb +93 -8
  99. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  100. data/lib/capybara/selenium/nodes/firefox_node.rb +37 -59
  101. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  102. data/lib/capybara/selenium/nodes/safari_node.rb +27 -54
  103. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  104. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  105. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  106. data/lib/capybara/selenium/patches/logs.rb +45 -0
  107. data/lib/capybara/server.rb +19 -3
  108. data/lib/capybara/server/animation_disabler.rb +2 -2
  109. data/lib/capybara/server/checker.rb +6 -2
  110. data/lib/capybara/server/middleware.rb +23 -13
  111. data/lib/capybara/session.rb +124 -106
  112. data/lib/capybara/session/config.rb +12 -10
  113. data/lib/capybara/session/matchers.rb +6 -6
  114. data/lib/capybara/spec/public/offset.js +6 -0
  115. data/lib/capybara/spec/public/test.js +94 -5
  116. data/lib/capybara/spec/session/all_spec.rb +84 -6
  117. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  118. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  119. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  120. data/lib/capybara/spec/session/attach_file_spec.rb +14 -6
  121. data/lib/capybara/spec/session/check_spec.rb +10 -4
  122. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  123. data/lib/capybara/spec/session/click_button_spec.rb +44 -1
  124. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  125. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  126. data/lib/capybara/spec/session/fill_in_spec.rb +37 -2
  127. data/lib/capybara/spec/session/find_spec.rb +60 -6
  128. data/lib/capybara/spec/session/first_spec.rb +1 -1
  129. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  130. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  131. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  132. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  133. data/lib/capybara/spec/session/has_css_spec.rb +35 -6
  134. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  135. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  136. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  137. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  138. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  139. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  140. data/lib/capybara/spec/session/has_text_spec.rb +47 -0
  141. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  142. data/lib/capybara/spec/session/node_spec.rb +574 -16
  143. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  144. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  145. data/lib/capybara/spec/session/scroll_spec.rb +1 -1
  146. data/lib/capybara/spec/session/select_spec.rb +5 -10
  147. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  148. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  149. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  150. data/lib/capybara/spec/session/window/window_spec.rb +10 -9
  151. data/lib/capybara/spec/spec_helper.rb +7 -2
  152. data/lib/capybara/spec/test_app.rb +26 -21
  153. data/lib/capybara/spec/views/animated.erb +49 -0
  154. data/lib/capybara/spec/views/form.erb +25 -4
  155. data/lib/capybara/spec/views/frame_child.erb +2 -1
  156. data/lib/capybara/spec/views/frame_one.erb +1 -0
  157. data/lib/capybara/spec/views/obscured.erb +9 -9
  158. data/lib/capybara/spec/views/offset.erb +32 -0
  159. data/lib/capybara/spec/views/react.erb +45 -0
  160. data/lib/capybara/spec/views/spatial.erb +31 -0
  161. data/lib/capybara/spec/views/with_animation.erb +29 -1
  162. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  163. data/lib/capybara/spec/views/with_html.erb +28 -2
  164. data/lib/capybara/spec/views/with_js.erb +2 -1
  165. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  166. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  167. data/lib/capybara/version.rb +1 -1
  168. data/lib/capybara/window.rb +10 -10
  169. data/spec/basic_node_spec.rb +6 -6
  170. data/spec/capybara_spec.rb +28 -28
  171. data/spec/dsl_spec.rb +16 -3
  172. data/spec/filter_set_spec.rb +5 -5
  173. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  174. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  175. data/spec/minitest_spec.rb +12 -2
  176. data/spec/minitest_spec_spec.rb +56 -45
  177. data/spec/rack_test_spec.rb +25 -12
  178. data/spec/regexp_dissassembler_spec.rb +53 -39
  179. data/spec/result_spec.rb +50 -54
  180. data/spec/rspec/features_spec.rb +1 -0
  181. data/spec/rspec/shared_spec_matchers.rb +78 -62
  182. data/spec/rspec_spec.rb +5 -5
  183. data/spec/sauce_spec_chrome.rb +1 -0
  184. data/spec/selector_spec.rb +26 -16
  185. data/spec/selenium_spec_chrome.rb +84 -5
  186. data/spec/selenium_spec_chrome_remote.rb +23 -8
  187. data/spec/selenium_spec_edge.rb +23 -8
  188. data/spec/selenium_spec_firefox.rb +16 -21
  189. data/spec/selenium_spec_firefox_remote.rb +4 -13
  190. data/spec/selenium_spec_ie.rb +23 -15
  191. data/spec/selenium_spec_safari.rb +17 -17
  192. data/spec/server_spec.rb +87 -42
  193. data/spec/session_spec.rb +11 -4
  194. data/spec/shared_selenium_node.rb +83 -0
  195. data/spec/shared_selenium_session.rb +62 -72
  196. data/spec/spec_helper.rb +43 -5
  197. metadata +114 -16
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
7
9
 
8
10
  def set_text(value, clear: nil, **_unused)
9
11
  super.tap do
@@ -13,22 +15,105 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
13
15
  end
14
16
 
15
17
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
16
- super(value)
17
- rescue ::Selenium::WebDriver::Error::ExpectedError => err
18
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if err.message.match?(/File not found : .+\n.+/m)
18
+ # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
+ if browser_version >= 75.0
20
+ driver.execute_script(<<~JS, self)
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
+ arguments[0].value = null;
23
+ }
24
+ JS
25
+ end
26
+ super
27
+ rescue *file_errors => e
28
+ if e.message.match?(/File not found : .+\n.+/m)
29
+ raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
30
+ end
19
31
 
20
32
  raise
21
33
  end
22
34
 
23
- def drag_to(element)
24
- return super unless html5_draggable?
35
+ def drop(*args)
36
+ html5_drop(*args)
37
+ end
38
+
39
+ def click(*, **)
40
+ super
41
+ rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
42
+ raise
43
+ rescue ::Selenium::WebDriver::Error::WebDriverError => e
44
+ # chromedriver 74 (at least on mac) raises the wrong error for this
45
+ if e.message.match?(/element click intercepted/)
46
+ raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message
47
+ end
25
48
 
26
- html5_drag_to(element)
49
+ raise
50
+ end
51
+
52
+ def disabled?
53
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
54
+ end
55
+
56
+ def select_option
57
+ # To optimize to only one check and then click
58
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
59
+ arguments[0].matches(':disabled, select:disabled *, :checked')
60
+ JS
61
+ click unless selected_or_disabled
62
+ end
63
+
64
+ def visible?
65
+ return super unless native_displayed?
66
+
67
+ begin
68
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
69
+ rescue Selenium::WebDriver::Error::UnknownCommandError
70
+ # If the is_element_displayed command is unknown, no point in trying again
71
+ driver.options[:native_displayed] = false
72
+ super
73
+ end
27
74
  end
28
75
 
29
76
  private
30
77
 
31
- def bridge
32
- driver.browser.send(:bridge)
78
+ def perform_legacy_drag(element, drop_modifiers)
79
+ return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
80
+
81
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
82
+
83
+ # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
84
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
85
+ browser_action.release.perform
86
+ browser_action.click_and_hold(native).move_to(element.native).release.perform
87
+ end
88
+
89
+ def file_errors
90
+ @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
91
+ [::Selenium::WebDriver::Error::ExpectedError]
92
+ end
93
+ end
94
+
95
+ def browser_version(to_float = true)
96
+ caps = capabilities
97
+ ver = (caps[:browser_version] || caps[:version])
98
+ ver = ver.to_f if to_float
99
+ ver
100
+ end
101
+
102
+ def chromedriver_fixed_actions_key_state?
103
+ Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.68')
104
+ end
105
+
106
+ def chromedriver_supports_displayed_endpoint?
107
+ Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.25')
108
+ end
109
+
110
+ def chromedriver_version
111
+ capabilities['chrome']['chromedriverVersion'].split(' ')[0]
112
+ end
113
+
114
+ def native_displayed?
115
+ (driver.options[:native_displayed] != false) &&
116
+ (w3c? && chromedriver_supports_displayed_endpoint?) &&
117
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
33
118
  end
34
119
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/extensions/html5_drag'
4
+
5
+ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
6
+ include Html5Drag
7
+
8
+ def set_text(value, clear: nil, **_unused)
9
+ return super unless chrome_edge?
10
+
11
+ super.tap do
12
+ # React doesn't see the chromedriver element clear
13
+ send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
14
+ end
15
+ end
16
+
17
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
18
+ # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
19
+ if chrome_edge?
20
+ driver.execute_script(<<~JS, self)
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
+ arguments[0].value = null;
23
+ }
24
+ JS
25
+ end
26
+ super
27
+ rescue *file_errors => e
28
+ if e.message.match?(/File not found : .+\n.+/m)
29
+ raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload"
30
+ end
31
+
32
+ raise
33
+ end
34
+
35
+ def drop(*args)
36
+ return super unless chrome_edge?
37
+
38
+ html5_drop(*args)
39
+ end
40
+
41
+ def click(*)
42
+ super
43
+ rescue Selenium::WebDriver::Error::InvalidArgumentError => e
44
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
45
+ if tag_name == 'input' && type == 'file'
46
+ raise Selenium::WebDriver::Error::InvalidArgumentError, "EdgeChrome can't click on file inputs.\n#{e.message}"
47
+ end
48
+
49
+ raise
50
+ end
51
+
52
+ def disabled?
53
+ return super unless chrome_edge?
54
+
55
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
56
+ end
57
+
58
+ def select_option
59
+ return super unless chrome_edge?
60
+
61
+ # To optimize to only one check and then click
62
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
63
+ arguments[0].matches(':disabled, select:disabled *, :checked')
64
+ JS
65
+ click unless selected_or_disabled
66
+ end
67
+
68
+ def visible?
69
+ return super unless chrome_edge? && native_displayed?
70
+
71
+ begin
72
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
73
+ rescue Selenium::WebDriver::Error::UnknownCommandError
74
+ # If the is_element_displayed command is unknown, no point in trying again
75
+ driver.options[:native_displayed] = false
76
+ super
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def file_errors
83
+ @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do
84
+ [::Selenium::WebDriver::Error::ExpectedError]
85
+ end
86
+ end
87
+
88
+ def browser_version
89
+ @browser_version ||= begin
90
+ caps = driver.browser.capabilities
91
+ (caps[:browser_version] || caps[:version]).to_f
92
+ end
93
+ end
94
+
95
+ def chrome_edge?
96
+ browser_version >= 75
97
+ end
98
+
99
+ def native_displayed?
100
+ (driver.options[:native_displayed] != false) &&
101
+ # chromedriver_supports_displayed_endpoint? &&
102
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
103
+ end
104
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/file_input_click_emulation'
4
5
 
5
6
  class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
6
7
  include Html5Drag
8
+ include FileInputClickEmulation
7
9
 
8
10
  def click(keys = [], **options)
9
11
  super
@@ -12,28 +14,22 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
12
14
  warn 'You are attempting to click a table row which has issues in geckodriver/marionette - '\
13
15
  'see https://github.com/mozilla/geckodriver/issues/1228. Your test should probably be '\
14
16
  'clicking on a table cell like a user would. Clicking the first cell in the row instead.'
15
- return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
16
18
  end
17
19
  raise
18
20
  end
19
21
 
20
22
  def disabled?
21
- # Not sure exactly what version of FF fixed the below issue, but it is definitely fixed in 61+
22
- return super unless browser_version < 61.0
23
-
24
- return true if super
25
-
26
- # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
27
- if %w[option optgroup].include? tag_name
28
- find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
29
- else
30
- !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
31
- end
23
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
32
24
  end
33
25
 
34
26
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
35
27
  # By default files are appended so we have to clear here if its multiple and already set
36
- native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
28
+ driver.execute_script(<<~JS, self)
29
+ if (arguments[0].multiple && arguments[0].files.length){
30
+ arguments[0].value = null;
31
+ }
32
+ JS
37
33
  return super if browser_version >= 62.0
38
34
 
39
35
  # Workaround lack of support for multiple upload by uploading one at a time
@@ -46,16 +42,14 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
46
42
 
47
43
  def send_keys(*args)
48
44
  # https://github.com/mozilla/geckodriver/issues/846
49
- return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
45
+ return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none?(Array)
50
46
 
51
47
  native.click
52
48
  _send_keys(args).perform
53
49
  end
54
50
 
55
- def drag_to(element)
56
- return super unless (browser_version >= 62.0) && html5_draggable?
57
-
58
- html5_drag_to(element)
51
+ def drop(*args)
52
+ html5_drop(*args)
59
53
  end
60
54
 
61
55
  def hover
@@ -65,9 +59,33 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
65
59
  scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
66
60
  end
67
61
 
62
+ def select_option
63
+ # To optimize to only one check and then click
64
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
65
+ arguments[0].matches(':disabled, select:disabled *, :checked')
66
+ JS
67
+ click unless selected_or_disabled
68
+ end
69
+
70
+ def visible?
71
+ return super unless native_displayed?
72
+
73
+ begin
74
+ bridge.send(:execute, :is_element_displayed, id: native.ref)
75
+ rescue Selenium::WebDriver::Error::UnknownCommandError
76
+ # If the is_element_displayed command is unknown, no point in trying again
77
+ driver.options[:native_displayed] = false
78
+ super
79
+ end
80
+ end
81
+
68
82
  private
69
83
 
70
- def click_with_options(click_options)
84
+ def native_displayed?
85
+ (driver.options[:native_displayed] != false) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
86
+ end
87
+
88
+ def perform_with_options(click_options)
71
89
  # Firefox/marionette has an issue clicking with offset near viewport edge
72
90
  # scroll element to middle just in case
73
91
  scroll_to_center if click_options.coords?
@@ -99,10 +117,6 @@ private
99
117
  actions
100
118
  end
101
119
 
102
- def bridge
103
- driver.browser.send(:bridge)
104
- end
105
-
106
120
  def upload(local_file)
107
121
  return nil unless local_file
108
122
  raise ArgumentError, "You may only upload files: #{local_file.inspect}" unless File.file?(local_file)
@@ -114,40 +128,4 @@ private
114
128
  def browser_version
115
129
  driver.browser.capabilities[:browser_version].to_f
116
130
  end
117
-
118
- DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
119
- x.parent(:fieldset)[
120
- x.attr(:disabled)
121
- ] + x.ancestor[
122
- ~x.self(:legend) |
123
- x.preceding_sibling(:legend)
124
- ][
125
- x.parent(:fieldset)[
126
- x.attr(:disabled)
127
- ]
128
- ]
129
- end.to_s.freeze
130
-
131
- class ModifierKeysStack
132
- def initialize
133
- @stack = []
134
- end
135
-
136
- def include?(key)
137
- @stack.flatten.include?(key)
138
- end
139
-
140
- def press(key)
141
- @stack.last.push(key)
142
- end
143
-
144
- def push
145
- @stack.push []
146
- end
147
-
148
- def pop
149
- @stack.pop
150
- end
151
- end
152
- private_constant :ModifierKeysStack
153
131
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/extensions/html5_drag'
4
+
5
+ class Capybara::Selenium::IENode < Capybara::Selenium::Node
6
+ def disabled?
7
+ # super
8
+ # optimize to one script call
9
+ driver.evaluate_script <<~JS.delete("\n"), self
10
+ arguments[0].msMatchesSelector('
11
+ :disabled,
12
+ select:disabled *,
13
+ optgroup:disabled *,
14
+ fieldset[disabled],
15
+ fieldset[disabled] > *:not(legend),
16
+ fieldset[disabled] > *:not(legend) *,
17
+ fieldset[disabled] > legend:nth-of-type(n+2),
18
+ fieldset[disabled] > legend:nth-of-type(n+2) *
19
+ ')
20
+ JS
21
+ end
22
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/modifier_keys_stack'
4
5
 
5
6
  class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
6
7
  # include Html5Drag
@@ -16,11 +17,21 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
16
17
  return find_css('th:first-child,td:first-child')[0].click(keys, options)
17
18
  end
18
19
  raise
20
+ rescue ::Selenium::WebDriver::Error::WebDriverError => e
21
+ raise unless e.instance_of? ::Selenium::WebDriver::Error::WebDriverError
22
+
23
+ # Safari doesn't return a specific error here - assume it's an ElementNotInteractableError
24
+ raise ::Selenium::WebDriver::Error::ElementNotInteractableError,
25
+ 'Non distinct error raised in #click, translated to ElementNotInteractableError for retry'
19
26
  end
20
27
 
21
28
  def select_option
22
- driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
23
- super
29
+ # To optimize to only one check and then click
30
+ selected_or_disabled = driver.execute_script(<<~JS, self)
31
+ arguments[0].closest('select').scrollIntoView();
32
+ return arguments[0].matches(':disabled, select:disabled *, :checked');
33
+ JS
34
+ click unless selected_or_disabled
24
35
  end
25
36
 
26
37
  def unselect_option
@@ -40,14 +51,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
40
51
  end
41
52
 
42
53
  def disabled?
43
- return true if super || (self[:disabled] == 'true')
44
-
45
- # workaround for safaridriver reporting elements as enabled when they are nested in disabling elements
46
- if %w[option optgroup].include? tag_name
47
- find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
48
- else
49
- !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
50
- end
54
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
51
55
  end
52
56
 
53
57
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
@@ -57,7 +61,9 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
57
61
  end
58
62
 
59
63
  def send_keys(*args)
60
- return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
64
+ if args.none? { |arg| arg.is_a?(Array) || (arg.is_a?(Symbol) && MODIFIER_KEYS.include?(arg)) }
65
+ return super(*args.map { |arg| arg == :space ? ' ' : arg })
66
+ end
61
67
 
62
68
  native.click
63
69
  _send_keys(args).perform
@@ -77,32 +83,16 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
77
83
  end
78
84
  end
79
85
 
80
- private
81
-
82
- def bridge
83
- driver.browser.send(:bridge)
86
+ def hover
87
+ # Workaround issue where hover would sometimes fail - possibly due to mouse not having moved
88
+ scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
84
89
  end
85
90
 
86
- DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
87
- x.parent(:fieldset)[
88
- x.attr(:disabled)
89
- ] + x.ancestor[
90
- ~x.self(:legend) |
91
- x.preceding_sibling(:legend)
92
- ][
93
- x.parent(:fieldset)[
94
- x.attr(:disabled)
95
- ]
96
- ]
97
- end.to_s.freeze
91
+ private
98
92
 
99
93
  def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
100
94
  case keys
101
- when :control, :left_control, :right_control,
102
- :alt, :left_alt, :right_alt,
103
- :shift, :left_shift, :right_shift,
104
- :meta, :left_meta, :right_meta,
105
- :command
95
+ when *MODIFIER_KEYS
106
96
  down_keys.press(keys)
107
97
  actions.key_down(keys)
108
98
  when String
@@ -120,26 +110,9 @@ private
120
110
  actions
121
111
  end
122
112
 
123
- class ModifierKeysStack
124
- def initialize
125
- @stack = []
126
- end
127
-
128
- def include?(key)
129
- @stack.flatten.include?(key)
130
- end
131
-
132
- def press(key)
133
- @stack.last.push(key)
134
- end
135
-
136
- def push
137
- @stack.push []
138
- end
139
-
140
- def pop
141
- @stack.pop
142
- end
143
- end
144
- private_constant :ModifierKeysStack
113
+ MODIFIER_KEYS = %i[control left_control right_control
114
+ alt left_alt right_alt
115
+ shift left_shift right_shift
116
+ meta left_meta right_meta
117
+ command].freeze
145
118
  end