capybara 3.13.2 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +587 -16
  4. data/README.md +240 -90
  5. data/lib/capybara/config.rb +24 -11
  6. data/lib/capybara/cucumber.rb +1 -1
  7. data/lib/capybara/driver/base.rb +8 -0
  8. data/lib/capybara/driver/node.rb +20 -4
  9. data/lib/capybara/dsl.rb +5 -3
  10. data/lib/capybara/helpers.rb +25 -4
  11. data/lib/capybara/minitest/spec.rb +174 -90
  12. data/lib/capybara/minitest.rb +256 -142
  13. data/lib/capybara/node/actions.rb +123 -77
  14. data/lib/capybara/node/base.rb +20 -12
  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 +223 -117
  18. data/lib/capybara/node/finders.rb +81 -71
  19. data/lib/capybara/node/matchers.rb +271 -134
  20. data/lib/capybara/node/simple.rb +18 -5
  21. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  22. data/lib/capybara/queries/active_element_query.rb +18 -0
  23. data/lib/capybara/queries/ancestor_query.rb +8 -9
  24. data/lib/capybara/queries/base_query.rb +3 -2
  25. data/lib/capybara/queries/current_path_query.rb +15 -5
  26. data/lib/capybara/queries/selector_query.rb +364 -54
  27. data/lib/capybara/queries/sibling_query.rb +8 -6
  28. data/lib/capybara/queries/style_query.rb +2 -2
  29. data/lib/capybara/queries/text_query.rb +13 -1
  30. data/lib/capybara/queries/title_query.rb +1 -1
  31. data/lib/capybara/rack_test/browser.rb +76 -11
  32. data/lib/capybara/rack_test/driver.rb +10 -5
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +31 -9
  35. data/lib/capybara/rack_test/node.rb +74 -23
  36. data/lib/capybara/registration_container.rb +41 -0
  37. data/lib/capybara/registrations/drivers.rb +42 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  39. data/lib/capybara/registrations/servers.rb +66 -0
  40. data/lib/capybara/result.rb +44 -20
  41. data/lib/capybara/rspec/matcher_proxies.rb +13 -11
  42. data/lib/capybara/rspec/matchers/base.rb +31 -16
  43. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  44. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  45. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  46. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  47. data/lib/capybara/rspec/matchers/have_selector.rb +21 -21
  48. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  49. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  50. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  51. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  52. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  53. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  54. data/lib/capybara/rspec/matchers.rb +111 -68
  55. data/lib/capybara/rspec.rb +2 -0
  56. data/lib/capybara/selector/builders/css_builder.rb +11 -7
  57. data/lib/capybara/selector/builders/xpath_builder.rb +5 -3
  58. data/lib/capybara/selector/css.rb +11 -9
  59. data/lib/capybara/selector/definition/button.rb +68 -0
  60. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  61. data/lib/capybara/selector/definition/css.rb +10 -0
  62. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  63. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  64. data/lib/capybara/selector/definition/element.rb +28 -0
  65. data/lib/capybara/selector/definition/field.rb +40 -0
  66. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  67. data/lib/capybara/selector/definition/file_field.rb +13 -0
  68. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  69. data/lib/capybara/selector/definition/frame.rb +17 -0
  70. data/lib/capybara/selector/definition/id.rb +6 -0
  71. data/lib/capybara/selector/definition/label.rb +62 -0
  72. data/lib/capybara/selector/definition/link.rb +55 -0
  73. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  74. data/lib/capybara/selector/definition/option.rb +27 -0
  75. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  76. data/lib/capybara/selector/definition/select.rb +81 -0
  77. data/lib/capybara/selector/definition/table.rb +109 -0
  78. data/lib/capybara/selector/definition/table_row.rb +21 -0
  79. data/lib/capybara/selector/definition/xpath.rb +5 -0
  80. data/lib/capybara/selector/definition.rb +280 -0
  81. data/lib/capybara/selector/filter_set.rb +19 -18
  82. data/lib/capybara/selector/filters/base.rb +11 -2
  83. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  84. data/lib/capybara/selector/regexp_disassembler.rb +11 -7
  85. data/lib/capybara/selector/selector.rb +50 -440
  86. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  87. data/lib/capybara/selector.rb +473 -482
  88. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  89. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  90. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  91. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  92. data/lib/capybara/selenium/driver.rb +174 -62
  93. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +74 -18
  94. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  95. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +37 -3
  96. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  97. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  98. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  99. data/lib/capybara/selenium/extensions/find.rb +68 -45
  100. data/lib/capybara/selenium/extensions/html5_drag.rb +192 -22
  101. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  102. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  103. data/lib/capybara/selenium/node.rb +268 -72
  104. data/lib/capybara/selenium/nodes/chrome_node.rb +105 -9
  105. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  106. data/lib/capybara/selenium/nodes/firefox_node.rb +51 -61
  107. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  108. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  109. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  110. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  111. data/lib/capybara/selenium/patches/logs.rb +45 -0
  112. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  113. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  114. data/lib/capybara/server/animation_disabler.rb +43 -21
  115. data/lib/capybara/server/checker.rb +6 -2
  116. data/lib/capybara/server/middleware.rb +25 -13
  117. data/lib/capybara/server.rb +20 -4
  118. data/lib/capybara/session/config.rb +15 -11
  119. data/lib/capybara/session/matchers.rb +11 -11
  120. data/lib/capybara/session.rb +162 -131
  121. data/lib/capybara/spec/public/offset.js +6 -0
  122. data/lib/capybara/spec/public/test.js +105 -6
  123. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  124. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  125. data/lib/capybara/spec/session/all_spec.rb +89 -15
  126. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  127. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  128. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  129. data/lib/capybara/spec/session/attach_file_spec.rb +64 -31
  130. data/lib/capybara/spec/session/check_spec.rb +26 -4
  131. data/lib/capybara/spec/session/choose_spec.rb +14 -2
  132. data/lib/capybara/spec/session/click_button_spec.rb +109 -61
  133. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  134. data/lib/capybara/spec/session/click_link_spec.rb +23 -1
  135. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  136. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  137. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  138. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  139. data/lib/capybara/spec/session/fill_in_spec.rb +46 -5
  140. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  141. data/lib/capybara/spec/session/find_spec.rb +80 -7
  142. data/lib/capybara/spec/session/first_spec.rb +2 -2
  143. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  144. data/lib/capybara/spec/session/frame/within_frame_spec.rb +14 -1
  145. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  146. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  147. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  148. data/lib/capybara/spec/session/has_button_spec.rb +81 -0
  149. data/lib/capybara/spec/session/has_css_spec.rb +45 -8
  150. data/lib/capybara/spec/session/has_current_path_spec.rb +22 -7
  151. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  152. data/lib/capybara/spec/session/has_field_spec.rb +59 -1
  153. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  154. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  155. data/lib/capybara/spec/session/has_select_spec.rb +42 -8
  156. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  157. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  158. data/lib/capybara/spec/session/has_table_spec.rb +177 -0
  159. data/lib/capybara/spec/session/has_text_spec.rb +31 -3
  160. data/lib/capybara/spec/session/html_spec.rb +1 -1
  161. data/lib/capybara/spec/session/matches_style_spec.rb +6 -4
  162. data/lib/capybara/spec/session/node_spec.rb +697 -23
  163. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  164. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  165. data/lib/capybara/spec/session/reset_session_spec.rb +21 -7
  166. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  167. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  168. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  169. data/lib/capybara/spec/session/scroll_spec.rb +9 -7
  170. data/lib/capybara/spec/session/select_spec.rb +5 -10
  171. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  172. data/lib/capybara/spec/session/uncheck_spec.rb +3 -3
  173. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  174. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  175. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  176. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  178. data/lib/capybara/spec/session/window/window_spec.rb +54 -57
  179. data/lib/capybara/spec/session/window/windows_spec.rb +2 -2
  180. data/lib/capybara/spec/session/within_spec.rb +36 -0
  181. data/lib/capybara/spec/spec_helper.rb +30 -19
  182. data/lib/capybara/spec/test_app.rb +122 -34
  183. data/lib/capybara/spec/views/animated.erb +49 -0
  184. data/lib/capybara/spec/views/form.erb +86 -8
  185. data/lib/capybara/spec/views/frame_child.erb +3 -2
  186. data/lib/capybara/spec/views/frame_one.erb +2 -1
  187. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  188. data/lib/capybara/spec/views/frame_two.erb +1 -1
  189. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  190. data/lib/capybara/spec/views/layout.erb +10 -0
  191. data/lib/capybara/spec/views/obscured.erb +10 -10
  192. data/lib/capybara/spec/views/offset.erb +33 -0
  193. data/lib/capybara/spec/views/path.erb +2 -2
  194. data/lib/capybara/spec/views/popup_one.erb +1 -1
  195. data/lib/capybara/spec/views/popup_two.erb +1 -1
  196. data/lib/capybara/spec/views/react.erb +45 -0
  197. data/lib/capybara/spec/views/scroll.erb +2 -1
  198. data/lib/capybara/spec/views/spatial.erb +31 -0
  199. data/lib/capybara/spec/views/tables.erb +67 -0
  200. data/lib/capybara/spec/views/with_animation.erb +39 -4
  201. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  202. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  203. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  204. data/lib/capybara/spec/views/with_hover.erb +3 -2
  205. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  206. data/lib/capybara/spec/views/with_html.erb +34 -6
  207. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  208. data/lib/capybara/spec/views/with_js.erb +7 -4
  209. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  210. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  211. data/lib/capybara/spec/views/with_scope.erb +2 -2
  212. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  213. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  214. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  215. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  216. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  217. data/lib/capybara/spec/views/with_windows.erb +1 -1
  218. data/lib/capybara/spec/views/within_frames.erb +1 -1
  219. data/lib/capybara/version.rb +1 -1
  220. data/lib/capybara/window.rb +14 -18
  221. data/lib/capybara.rb +91 -126
  222. data/spec/basic_node_spec.rb +30 -16
  223. data/spec/capybara_spec.rb +40 -28
  224. data/spec/counter_spec.rb +35 -0
  225. data/spec/css_builder_spec.rb +3 -1
  226. data/spec/css_splitter_spec.rb +1 -1
  227. data/spec/dsl_spec.rb +33 -22
  228. data/spec/filter_set_spec.rb +5 -5
  229. data/spec/fixtures/selenium_driver_rspec_failure.rb +3 -3
  230. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  231. data/spec/minitest_spec.rb +24 -2
  232. data/spec/minitest_spec_spec.rb +60 -45
  233. data/spec/per_session_config_spec.rb +1 -1
  234. data/spec/rack_test_spec.rb +131 -98
  235. data/spec/regexp_dissassembler_spec.rb +53 -39
  236. data/spec/result_spec.rb +68 -66
  237. data/spec/rspec/features_spec.rb +9 -4
  238. data/spec/rspec/scenarios_spec.rb +6 -2
  239. data/spec/rspec/shared_spec_matchers.rb +137 -98
  240. data/spec/rspec_matchers_spec.rb +25 -0
  241. data/spec/rspec_spec.rb +23 -21
  242. data/spec/sauce_spec_chrome.rb +43 -0
  243. data/spec/selector_spec.rb +77 -21
  244. data/spec/selenium_spec_chrome.rb +141 -39
  245. data/spec/selenium_spec_chrome_remote.rb +32 -17
  246. data/spec/selenium_spec_edge.rb +36 -8
  247. data/spec/selenium_spec_firefox.rb +110 -68
  248. data/spec/selenium_spec_firefox_remote.rb +22 -15
  249. data/spec/selenium_spec_ie.rb +29 -22
  250. data/spec/selenium_spec_safari.rb +162 -0
  251. data/spec/server_spec.rb +153 -81
  252. data/spec/session_spec.rb +11 -4
  253. data/spec/shared_selenium_node.rb +79 -0
  254. data/spec/shared_selenium_session.rb +179 -74
  255. data/spec/spec_helper.rb +80 -5
  256. data/spec/whitespace_normalizer_spec.rb +54 -0
  257. data/spec/xpath_builder_spec.rb +3 -1
  258. metadata +218 -30
  259. data/lib/capybara/spec/session/source_spec.rb +0 -0
  260. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -0,0 +1,110 @@
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
+ end
28
+
29
+ def drop(*args)
30
+ return super unless chrome_edge?
31
+
32
+ html5_drop(*args)
33
+ end
34
+
35
+ def click(*, **)
36
+ super
37
+ rescue Selenium::WebDriver::Error::InvalidArgumentError => e
38
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
39
+ if tag_name == 'input' && type == 'file'
40
+ raise Selenium::WebDriver::Error::InvalidArgumentError, "EdgeChrome can't click on file inputs.\n#{e.message}"
41
+ end
42
+
43
+ raise
44
+ end
45
+
46
+ def disabled?
47
+ return super unless chrome_edge?
48
+
49
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
50
+ end
51
+
52
+ def select_option
53
+ return super unless chrome_edge?
54
+
55
+ # To optimize to only one check and then click
56
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
57
+ arguments[0].matches(':disabled, select:disabled *, :checked')
58
+ JS
59
+ click unless selected_or_disabled
60
+ end
61
+
62
+ def visible?
63
+ return super unless chrome_edge? && native_displayed?
64
+
65
+ begin
66
+ bridge.send(:execute, :is_element_displayed, id: native_id)
67
+ rescue Selenium::WebDriver::Error::UnknownCommandError
68
+ # If the is_element_displayed command is unknown, no point in trying again
69
+ driver.options[:native_displayed] = false
70
+ super
71
+ end
72
+ end
73
+
74
+ def send_keys(*args)
75
+ args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
76
+ .each do |contains_emoji, inputs|
77
+ if contains_emoji
78
+ inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
79
+ .each do |emoji, clusters|
80
+ if emoji
81
+ driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
82
+ else
83
+ super(clusters.join)
84
+ end
85
+ end
86
+ else
87
+ super(*inputs)
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def browser_version
95
+ @browser_version ||= begin
96
+ caps = driver.browser.capabilities
97
+ (caps[:browser_version] || caps[:version]).to_f
98
+ end
99
+ end
100
+
101
+ def chrome_edge?
102
+ browser_version >= 75
103
+ end
104
+
105
+ def native_displayed?
106
+ (driver.options[:native_displayed] != false) &&
107
+ # chromedriver_supports_displayed_endpoint? &&
108
+ (!ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'])
109
+ end
110
+ end
@@ -1,39 +1,35 @@
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
10
12
  rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
11
13
  if tag_name == 'tr'
12
- warn 'You are attempting to click a table row which has issues in geckodriver/marionette - '\
13
- 'see https://github.com/mozilla/geckodriver/issues/1228. Your test should probably be '\
14
+ warn 'You are attempting to click a table row which has issues in geckodriver/marionette - ' \
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
@@ -44,23 +40,57 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
44
40
  path_names.each { |path| native.send_keys(path) }
45
41
  end
46
42
 
43
+ def focused?
44
+ driver.evaluate_script('arguments[0] == document.activeElement', self)
45
+ end
46
+
47
47
  def send_keys(*args)
48
48
  # https://github.com/mozilla/geckodriver/issues/846
49
- return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
49
+ return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none?(Array)
50
+
51
+ native.click unless focused?
50
52
 
51
- native.click
52
53
  _send_keys(args).perform
53
54
  end
54
55
 
55
- def drag_to(element)
56
- return super unless (browser_version >= 62.0) && html5_draggable?
56
+ def drop(*args)
57
+ html5_drop(*args)
58
+ end
57
59
 
58
- html5_drag_to(element)
60
+ def hover
61
+ return super unless browser_version >= 65.0
62
+
63
+ # work around issue 2156 - https://github.com/teamcapybara/capybara/issues/2156
64
+ scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
65
+ end
66
+
67
+ def select_option
68
+ # To optimize to only one check and then click
69
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
70
+ arguments[0].matches(':disabled, select:disabled *, :checked')
71
+ JS
72
+ click unless selected_or_disabled
73
+ end
74
+
75
+ def visible?
76
+ return super unless native_displayed?
77
+
78
+ begin
79
+ bridge.send(:execute, :is_element_displayed, id: native_id)
80
+ rescue Selenium::WebDriver::Error::UnknownCommandError
81
+ # If the is_element_displayed command is unknown, no point in trying again
82
+ driver.options[:native_displayed] = false
83
+ super
84
+ end
59
85
  end
60
86
 
61
87
  private
62
88
 
63
- def click_with_options(click_options)
89
+ def native_displayed?
90
+ (driver.options[:native_displayed] != false) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
91
+ end
92
+
93
+ def perform_with_options(click_options)
64
94
  # Firefox/marionette has an issue clicking with offset near viewport edge
65
95
  # scroll element to middle just in case
66
96
  scroll_to_center if click_options.coords?
@@ -92,10 +122,6 @@ private
92
122
  actions
93
123
  end
94
124
 
95
- def bridge
96
- driver.browser.send(:bridge)
97
- end
98
-
99
125
  def upload(local_file)
100
126
  return nil unless local_file
101
127
  raise ArgumentError, "You may only upload files: #{local_file.inspect}" unless File.file?(local_file)
@@ -107,40 +133,4 @@ private
107
133
  def browser_version
108
134
  driver.browser.capabilities[:browser_version].to_f
109
135
  end
110
-
111
- DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
112
- x.parent(:fieldset)[
113
- x.attr(:disabled)
114
- ] + x.ancestor[
115
- ~x.self(:legned) |
116
- x.preceding_sibling(:legend)
117
- ][
118
- x.parent(:fieldset)[
119
- x.attr(:disabled)
120
- ]
121
- ]
122
- end.to_s.freeze
123
-
124
- class ModifierKeysStack
125
- def initialize
126
- @stack = []
127
- end
128
-
129
- def include?(key)
130
- @stack.flatten.include?(key)
131
- end
132
-
133
- def press(key)
134
- @stack.last.push(key)
135
- end
136
-
137
- def push
138
- @stack.push []
139
- end
140
-
141
- def pop
142
- @stack.pop
143
- end
144
- end
145
- private_constant :ModifierKeysStack
146
136
  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
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'capybara/selenium/extensions/html5_drag'
4
+ require 'capybara/selenium/extensions/modifier_keys_stack'
5
+
6
+ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
7
+ # include Html5Drag
8
+
9
+ def click(keys = [], **options)
10
+ # driver.execute_script('arguments[0].scrollIntoViewIfNeeded({block: "center"})', self)
11
+ super
12
+ rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
13
+ if tag_name == 'tr'
14
+ warn 'You are attempting to click a table row which has issues in safaridriver - ' \
15
+ 'Your test should probably be clicking on a table cell like a user would. ' \
16
+ 'Clicking the first cell in the row instead.'
17
+ return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
+ end
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'
26
+ end
27
+
28
+ def select_option
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
35
+ end
36
+
37
+ def unselect_option
38
+ driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
39
+ super
40
+ end
41
+
42
+ def visible_text
43
+ return '' unless visible?
44
+
45
+ vis_text = driver.execute_script('return arguments[0].innerText', self)
46
+ vis_text.squeeze(' ')
47
+ .gsub(/[\ \n]*\n[\ \n]*/, "\n")
48
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
49
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
50
+ .tr("\u00a0", ' ')
51
+ end
52
+
53
+ def disabled?
54
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
55
+ end
56
+
57
+ def set_file(value) # rubocop:disable Naming/AccessorMethodName
58
+ # By default files are appended so we have to clear here if its multiple and already set
59
+ native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
60
+ super
61
+ end
62
+
63
+ def send_keys(*args)
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
67
+
68
+ native.click
69
+ _send_keys(args).perform
70
+ end
71
+
72
+ def set_text(value, clear: nil, **_unused)
73
+ value = value.to_s
74
+ if clear == :backspace
75
+ # Clear field by sending the correct number of backspace keys.
76
+ backspaces = [:backspace] * self.value.to_s.length
77
+ send_keys([:control, 'e'], *backspaces, value)
78
+ else
79
+ super.tap do
80
+ # React doesn't see the safaridriver element clear
81
+ send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
82
+ end
83
+ end
84
+ end
85
+
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 }
89
+ end
90
+
91
+ private
92
+
93
+ def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
94
+ case keys
95
+ when *MODIFIER_KEYS
96
+ down_keys.press(keys)
97
+ actions.key_down(keys)
98
+ when String
99
+ keys = keys.upcase if down_keys&.include?(:shift)
100
+ actions.send_keys(keys)
101
+ when Symbol
102
+ actions.send_keys(keys)
103
+ when Array
104
+ down_keys.push
105
+ keys.each { |sub_keys| _send_keys(sub_keys, actions, down_keys) }
106
+ down_keys.pop.reverse_each { |key| actions.key_up(key) }
107
+ else
108
+ raise ArgumentError, 'Unknown keys type'
109
+ end
110
+ actions
111
+ end
112
+
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
118
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CapybaraAtoms
4
+ private
5
+
6
+ def read_atom(function)
7
+ @atoms ||= Hash.new do |hash, key|
8
+ hash[key] = begin
9
+ File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
10
+ rescue Errno::ENOENT
11
+ super
12
+ end
13
+ end
14
+ @atoms[function]
15
+ end
16
+ end
17
+
18
+ Selenium::WebDriver::Remote::Bridge.prepend CapybaraAtoms unless ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module IsDisplayed
6
+ def commands(command)
7
+ case command
8
+ when :is_element_displayed
9
+ [:get, 'session/:session_id/element/:id/displayed']
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ module ChromeLogs
6
+ LOG_MSG = <<~MSG
7
+ Chromedriver 75+ defaults to W3C mode. Please upgrade to chromedriver >= \
8
+ 75.0.3770.90 if you need to access logs while in W3C compliant mode.
9
+ MSG
10
+
11
+ COMMANDS = {
12
+ get_available_log_types: [:get, 'session/:session_id/se/log/types'],
13
+ get_log: [:post, 'session/:session_id/se/log'],
14
+ get_log_legacy: [:post, 'session/:session_id/log']
15
+ }.freeze
16
+
17
+ def commands(command)
18
+ COMMANDS[command] || super
19
+ end
20
+
21
+ def available_log_types
22
+ types = execute :get_available_log_types
23
+ Array(types).map(&:to_sym)
24
+ rescue ::Selenium::WebDriver::Error::UnknownCommandError
25
+ raise NotImplementedError, LOG_MSG
26
+ end
27
+
28
+ def log(type)
29
+ data = begin
30
+ execute :get_log, {}, type: type.to_s
31
+ rescue ::Selenium::WebDriver::Error::UnknownCommandError
32
+ execute :get_log_legacy, {}, type: type.to_s
33
+ end
34
+
35
+ Array(data).map do |l|
36
+ ::Selenium::WebDriver::LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message')
37
+ rescue KeyError
38
+ next
39
+ end
40
+ rescue ::Selenium::WebDriver::Error::UnknownCommandError
41
+ raise NotImplementedError, LOG_MSG
42
+ end
43
+ end
44
+ end
45
+ end
@@ -6,4 +6,4 @@ module PauseDurationFix
6
6
  end
7
7
  end
8
8
 
9
- ::Selenium::WebDriver::Interactions::Pause.prepend PauseDurationFix
9
+ Selenium::WebDriver::Interactions::Pause.prepend PauseDurationFix
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Selenium
5
+ class PersistentClient < ::Selenium::WebDriver::Remote::Http::Default
6
+ def close
7
+ super
8
+ @http.finish if @http&.started?
9
+ end
10
+
11
+ private
12
+
13
+ def http
14
+ super.tap do |http|
15
+ http.start unless http.started?
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -16,43 +16,65 @@ module Capybara
16
16
 
17
17
  def initialize(app)
18
18
  @app = app
19
- @disable_markup = format(DISABLE_MARKUP_TEMPLATE, selector: self.class.selector_for(Capybara.disable_animation))
19
+ @disable_css_markup = format(DISABLE_CSS_MARKUP_TEMPLATE,
20
+ selector: self.class.selector_for(Capybara.disable_animation))
21
+ @disable_js_markup = +DISABLE_JS_MARKUP_TEMPLATE
20
22
  end
21
23
 
22
24
  def call(env)
23
- @status, @headers, @body = @app.call(env)
24
- return [@status, @headers, @body] unless html_content?
25
+ status, headers, body = @app.call(env)
26
+ return [status, headers, body] unless html_content?(headers)
25
27
 
26
- response = Rack::Response.new([], @status, @headers)
28
+ nonces = directive_nonces(headers).transform_values { |nonce| "nonce=\"#{nonce}\"" if nonce && !nonce.empty? }
29
+ response = Rack::Response.new([], status, headers)
27
30
 
28
- @body.each { |html| response.write insert_disable(html) }
29
- @body.close if @body.respond_to?(:close)
31
+ body.each { |html| response.write insert_disable(html, nonces) }
32
+ body.close if body.respond_to?(:close)
30
33
 
31
34
  response.finish
32
35
  end
33
36
 
34
37
  private
35
38
 
36
- attr_reader :disable_markup
39
+ attr_reader :disable_css_markup, :disable_js_markup
37
40
 
38
- def html_content?
39
- !!(@headers['Content-Type'] =~ /html/)
41
+ def html_content?(headers)
42
+ /html/.match?(headers['Content-Type']) # rubocop:todo Performance/StringInclude
40
43
  end
41
44
 
42
- def insert_disable(html)
43
- html.sub(%r{(</head>)}, disable_markup + '\\1')
45
+ def insert_disable(html, nonces)
46
+ html.sub(%r{(</head>)}, "<style #{nonces['style-src']}>#{disable_css_markup}</style>\\1")
47
+ .sub(%r{(</body>)}, "<script #{nonces['script-src']}>#{disable_js_markup}</script>\\1")
44
48
  end
45
49
 
46
- DISABLE_MARKUP_TEMPLATE = <<~HTML
47
- <script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
48
- <style>
49
- %<selector>s {
50
- transition: none !important;
51
- animation-duration: 0s !important;
52
- animation-delay: 0s !important;
53
- }
54
- </style>
55
- HTML
50
+ def directive_nonces(headers)
51
+ headers.fetch('Content-Security-Policy', '')
52
+ .split(';')
53
+ .map(&:split) # rubocop:disable Style/MapToHash
54
+ .to_h do |s|
55
+ [
56
+ s[0], s[1..].filter_map do |value|
57
+ /^'nonce-(?<nonce>.+)'/ =~ value
58
+ nonce
59
+ end[0]
60
+ ]
61
+ end
62
+ end
63
+
64
+ DISABLE_CSS_MARKUP_TEMPLATE = <<~CSS
65
+ %<selector>s, %<selector>s::before, %<selector>s::after {
66
+ transition: none !important;
67
+ animation-duration: 0s !important;
68
+ animation-delay: 0s !important;
69
+ scroll-behavior: auto !important;
70
+ }
71
+ CSS
72
+
73
+ DISABLE_JS_MARKUP_TEMPLATE = <<~SCRIPT
74
+ //<![CDATA[
75
+ (typeof jQuery !== 'undefined') && (jQuery.fx.off = true);
76
+ //]]>
77
+ SCRIPT
56
78
  end
57
79
  end
58
80
  end
@@ -25,11 +25,15 @@ module Capybara
25
25
  private
26
26
 
27
27
  def http_request(&block)
28
- Net::HTTP.start(@host, @port, read_timeout: 2, &block)
28
+ make_request(read_timeout: 2, &block)
29
29
  end
30
30
 
31
31
  def https_request(&block)
32
- Net::HTTP.start(@host, @port, ssl_options, &block)
32
+ make_request(**ssl_options, &block)
33
+ end
34
+
35
+ def make_request(**options, &block)
36
+ Net::HTTP.start(@host, @port, options.merge(max_retries: 0), &block)
33
37
  end
34
38
 
35
39
  def ssl_options