capybara 3.29.0 → 3.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +229 -15
  3. data/README.md +13 -4
  4. data/lib/capybara/config.rb +24 -10
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/driver/base.rb +8 -0
  7. data/lib/capybara/driver/node.rb +5 -1
  8. data/lib/capybara/dsl.rb +5 -3
  9. data/lib/capybara/helpers.rb +19 -2
  10. data/lib/capybara/minitest/spec.rb +156 -97
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/node/actions.rb +41 -37
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/document.rb +2 -2
  15. data/lib/capybara/node/document_matchers.rb +3 -3
  16. data/lib/capybara/node/element.rb +35 -21
  17. data/lib/capybara/node/finders.rb +33 -19
  18. data/lib/capybara/node/matchers.rb +72 -57
  19. data/lib/capybara/node/simple.rb +13 -3
  20. data/lib/capybara/queries/active_element_query.rb +18 -0
  21. data/lib/capybara/queries/ancestor_query.rb +4 -3
  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 +91 -30
  25. data/lib/capybara/queries/sibling_query.rb +4 -3
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +7 -1
  28. data/lib/capybara/rack_test/browser.rb +68 -10
  29. data/lib/capybara/rack_test/driver.rb +6 -5
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +44 -16
  32. data/lib/capybara/registration_container.rb +41 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  35. data/lib/capybara/registrations/servers.rb +3 -2
  36. data/lib/capybara/result.rb +35 -15
  37. data/lib/capybara/rspec/matcher_proxies.rb +8 -8
  38. data/lib/capybara/rspec/matchers/base.rb +12 -6
  39. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  40. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  41. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  42. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  43. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  45. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  46. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  47. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  48. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  49. data/lib/capybara/rspec/matchers.rb +33 -32
  50. data/lib/capybara/rspec.rb +2 -0
  51. data/lib/capybara/selector/builders/css_builder.rb +2 -2
  52. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  53. data/lib/capybara/selector/css.rb +2 -2
  54. data/lib/capybara/selector/definition/button.rb +35 -13
  55. data/lib/capybara/selector/definition/checkbox.rb +3 -3
  56. data/lib/capybara/selector/definition/css.rb +3 -1
  57. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  58. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  59. data/lib/capybara/selector/definition/element.rb +3 -2
  60. data/lib/capybara/selector/definition/field.rb +1 -1
  61. data/lib/capybara/selector/definition/file_field.rb +2 -2
  62. data/lib/capybara/selector/definition/fillable_field.rb +3 -3
  63. data/lib/capybara/selector/definition/label.rb +5 -3
  64. data/lib/capybara/selector/definition/link.rb +8 -0
  65. data/lib/capybara/selector/definition/radio_button.rb +3 -3
  66. data/lib/capybara/selector/definition/select.rb +33 -14
  67. data/lib/capybara/selector/definition/table.rb +6 -3
  68. data/lib/capybara/selector/definition/table_row.rb +2 -2
  69. data/lib/capybara/selector/definition.rb +15 -11
  70. data/lib/capybara/selector/filter_set.rb +17 -17
  71. data/lib/capybara/selector/filters/base.rb +6 -1
  72. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  73. data/lib/capybara/selector/selector.rb +17 -3
  74. data/lib/capybara/selector.rb +37 -19
  75. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  76. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  77. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  78. data/lib/capybara/selenium/driver.rb +84 -17
  79. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +11 -13
  80. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +10 -12
  81. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +4 -4
  82. data/lib/capybara/selenium/extensions/find.rb +4 -4
  83. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  84. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  85. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  86. data/lib/capybara/selenium/node.rb +122 -26
  87. data/lib/capybara/selenium/nodes/chrome_node.rb +34 -19
  88. data/lib/capybara/selenium/nodes/edge_node.rb +5 -3
  89. data/lib/capybara/selenium/nodes/firefox_node.rb +11 -6
  90. data/lib/capybara/selenium/nodes/safari_node.rb +3 -3
  91. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  92. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  93. data/lib/capybara/selenium/patches/logs.rb +7 -9
  94. data/lib/capybara/server/animation_disabler.rb +38 -15
  95. data/lib/capybara/server/checker.rb +1 -1
  96. data/lib/capybara/server/middleware.rb +22 -10
  97. data/lib/capybara/server.rb +15 -3
  98. data/lib/capybara/session/config.rb +10 -4
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +62 -39
  101. data/lib/capybara/spec/public/test.js +75 -7
  102. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  103. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  104. data/lib/capybara/spec/session/all_spec.rb +63 -12
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  107. data/lib/capybara/spec/session/check_spec.rb +15 -0
  108. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  109. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  110. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  111. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  112. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  113. data/lib/capybara/spec/session/find_spec.rb +37 -8
  114. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  115. data/lib/capybara/spec/session/has_button_spec.rb +75 -0
  116. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  117. data/lib/capybara/spec/session/has_current_path_spec.rb +17 -4
  118. data/lib/capybara/spec/session/has_field_spec.rb +41 -1
  119. data/lib/capybara/spec/session/has_link_spec.rb +30 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +36 -8
  121. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +21 -1
  124. data/lib/capybara/spec/session/html_spec.rb +1 -1
  125. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  126. data/lib/capybara/spec/session/node_spec.rb +226 -33
  127. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  128. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  129. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  130. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  131. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  132. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  133. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  134. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  136. data/lib/capybara/spec/session/window/window_spec.rb +9 -9
  137. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  138. data/lib/capybara/spec/spec_helper.rb +17 -17
  139. data/lib/capybara/spec/test_app.rb +89 -29
  140. data/lib/capybara/spec/views/animated.erb +1 -1
  141. data/lib/capybara/spec/views/form.erb +52 -6
  142. data/lib/capybara/spec/views/frame_child.erb +1 -1
  143. data/lib/capybara/spec/views/frame_one.erb +1 -1
  144. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  145. data/lib/capybara/spec/views/frame_two.erb +1 -1
  146. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  147. data/lib/capybara/spec/views/layout.erb +10 -0
  148. data/lib/capybara/spec/views/obscured.erb +1 -1
  149. data/lib/capybara/spec/views/offset.erb +2 -1
  150. data/lib/capybara/spec/views/path.erb +2 -2
  151. data/lib/capybara/spec/views/popup_one.erb +1 -1
  152. data/lib/capybara/spec/views/popup_two.erb +1 -1
  153. data/lib/capybara/spec/views/react.erb +2 -2
  154. data/lib/capybara/spec/views/scroll.erb +2 -1
  155. data/lib/capybara/spec/views/spatial.erb +1 -1
  156. data/lib/capybara/spec/views/with_animation.erb +10 -3
  157. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  158. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  159. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  160. data/lib/capybara/spec/views/with_hover.erb +2 -2
  161. data/lib/capybara/spec/views/with_html.erb +3 -3
  162. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  163. data/lib/capybara/spec/views/with_js.erb +5 -3
  164. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  165. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  166. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  167. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  168. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  169. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  170. data/lib/capybara/spec/views/with_windows.erb +1 -1
  171. data/lib/capybara/spec/views/within_frames.erb +1 -1
  172. data/lib/capybara/version.rb +1 -1
  173. data/lib/capybara/window.rb +4 -8
  174. data/lib/capybara.rb +36 -29
  175. data/spec/basic_node_spec.rb +25 -11
  176. data/spec/capybara_spec.rb +1 -1
  177. data/spec/dsl_spec.rb +18 -5
  178. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  179. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  180. data/spec/minitest_spec.rb +3 -2
  181. data/spec/minitest_spec_spec.rb +46 -46
  182. data/spec/rack_test_spec.rb +43 -11
  183. data/spec/regexp_dissassembler_spec.rb +40 -36
  184. data/spec/result_spec.rb +53 -45
  185. data/spec/rspec/features_spec.rb +7 -4
  186. data/spec/rspec/scenarios_spec.rb +5 -1
  187. data/spec/rspec/shared_spec_matchers.rb +68 -56
  188. data/spec/rspec_spec.rb +8 -4
  189. data/spec/sauce_spec_chrome.rb +3 -3
  190. data/spec/selector_spec.rb +19 -4
  191. data/spec/selenium_spec_chrome.rb +49 -26
  192. data/spec/selenium_spec_chrome_remote.rb +13 -6
  193. data/spec/selenium_spec_firefox.rb +29 -17
  194. data/spec/selenium_spec_firefox_remote.rb +2 -2
  195. data/spec/selenium_spec_ie.rb +3 -6
  196. data/spec/selenium_spec_safari.rb +31 -19
  197. data/spec/server_spec.rb +88 -35
  198. data/spec/session_spec.rb +1 -1
  199. data/spec/shared_selenium_node.rb +21 -7
  200. data/spec/shared_selenium_session.rb +123 -21
  201. data/spec/spec_helper.rb +2 -2
  202. metadata +80 -21
  203. data/lib/capybara/spec/session/source_spec.rb +0 -0
  204. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -18,14 +18,16 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
18
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
19
  if browser_version >= 75.0
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
25
25
  end
26
26
  super
27
27
  rescue *file_errors => e
28
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
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
29
31
 
30
32
  raise
31
33
  end
@@ -34,13 +36,15 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
34
36
  html5_drop(*args)
35
37
  end
36
38
 
37
- def click(*)
39
+ def click(*, **)
38
40
  super
39
41
  rescue ::Selenium::WebDriver::Error::ElementClickInterceptedError
40
42
  raise
41
43
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
42
44
  # chromedriver 74 (at least on mac) raises the wrong error for this
43
- raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message if e.message.match?(/element click intercepted/)
45
+ if e.message.include?('element click intercepted')
46
+ raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message
47
+ end
44
48
 
45
49
  raise
46
50
  end
@@ -61,7 +65,7 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
61
65
  return super unless native_displayed?
62
66
 
63
67
  begin
64
- bridge.send(:execute, :is_element_displayed, id: native.ref)
68
+ bridge.send(:execute, :is_element_displayed, id: native_id)
65
69
  rescue Selenium::WebDriver::Error::UnknownCommandError
66
70
  # If the is_element_displayed command is unknown, no point in trying again
67
71
  driver.options[:native_displayed] = false
@@ -69,11 +73,31 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
69
73
  end
70
74
  end
71
75
 
76
+ def send_keys(*args)
77
+ args.chunk { |inp| inp.is_a?(String) && inp.match?(/\p{Emoji Presentation}/) }
78
+ .each do |contains_emoji, inputs|
79
+ if contains_emoji
80
+ inputs.join.grapheme_clusters.chunk { |gc| gc.match?(/\p{Emoji Presentation}/) }
81
+ .each do |emoji, clusters|
82
+ if emoji
83
+ driver.send(:execute_cdp, 'Input.insertText', text: clusters.join)
84
+ else
85
+ super(clusters.join)
86
+ end
87
+ end
88
+ else
89
+ super(*inputs)
90
+ end
91
+ end
92
+ end
93
+
72
94
  private
73
95
 
74
- def perform_legacy_drag(element)
96
+ def perform_legacy_drag(element, drop_modifiers)
75
97
  return super if chromedriver_fixed_actions_key_state? || !w3c? || element.obscured?
76
98
 
99
+ raise ArgumentError, 'Modifier keys are not supported while dragging in this version of Chrome.' unless drop_modifiers.empty?
100
+
77
101
  # W3C Chrome/chromedriver < 77 doesn't maintain mouse button state across actions API performs
78
102
  # https://bugs.chromium.org/p/chromedriver/issues/detail?id=2981
79
103
  browser_action.release.perform
@@ -86,12 +110,7 @@ private
86
110
  end
87
111
  end
88
112
 
89
- def w3c?
90
- (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
91
- capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
92
- end
93
-
94
- def browser_version(to_float = true)
113
+ def browser_version(to_float: true)
95
114
  caps = capabilities
96
115
  ver = (caps[:browser_version] || caps[:version])
97
116
  ver = ver.to_f if to_float
@@ -99,19 +118,15 @@ private
99
118
  end
100
119
 
101
120
  def chromedriver_fixed_actions_key_state?
102
- Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.68')
121
+ Gem::Requirement.new('>= 76.0.3809.68').satisfied_by?(chromedriver_version)
103
122
  end
104
123
 
105
124
  def chromedriver_supports_displayed_endpoint?
106
- Gem::Version.new(chromedriver_version) >= Gem::Version.new('76.0.3809.25')
125
+ Gem::Requirement.new('>= 76.0.3809.25').satisfied_by?(chromedriver_version)
107
126
  end
108
127
 
109
128
  def chromedriver_version
110
- capabilities['chrome']['chromedriverVersion'].split(' ')[0]
111
- end
112
-
113
- def capabilities
114
- driver.browser.capabilities
129
+ Gem::Version.new(capabilities['chrome']['chromedriverVersion'].split(' ')[0]) # rubocop:disable Style/RedundantArgument
115
130
  end
116
131
 
117
132
  def native_displayed?
@@ -18,14 +18,16 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
18
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
19
  if chrome_edge?
20
20
  driver.execute_script(<<~JS, self)
21
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
21
+ if (arguments[0].multiple && arguments[0].files.length){
22
22
  arguments[0].value = null;
23
23
  }
24
24
  JS
25
25
  end
26
26
  super
27
27
  rescue *file_errors => e
28
- raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
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
29
31
 
30
32
  raise
31
33
  end
@@ -67,7 +69,7 @@ class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node
67
69
  return super unless chrome_edge? && native_displayed?
68
70
 
69
71
  begin
70
- bridge.send(:execute, :is_element_displayed, id: native.ref)
72
+ bridge.send(:execute, :is_element_displayed, id: native_id)
71
73
  rescue Selenium::WebDriver::Error::UnknownCommandError
72
74
  # If the is_element_displayed command is unknown, no point in trying again
73
75
  driver.options[:native_displayed] = false
@@ -14,7 +14,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in geckodriver/marionette - '\
15
15
  'see https://github.com/mozilla/geckodriver/issues/1228. Your test should probably be '\
16
16
  'clicking on a table cell like a user would. Clicking the first cell in the row instead.'
17
- 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)
18
18
  end
19
19
  raise
20
20
  end
@@ -26,7 +26,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
26
26
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
27
27
  # By default files are appended so we have to clear here if its multiple and already set
28
28
  driver.execute_script(<<~JS, self)
29
- if (arguments[0].multiple && (arguments[0].files.length > 0)){
29
+ if (arguments[0].multiple && arguments[0].files.length){
30
30
  arguments[0].value = null;
31
31
  }
32
32
  JS
@@ -40,11 +40,16 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
40
40
  path_names.each { |path| native.send_keys(path) }
41
41
  end
42
42
 
43
+ def focused?
44
+ driver.evaluate_script('arguments[0] == document.activeElement', self)
45
+ end
46
+
43
47
  def send_keys(*args)
44
48
  # https://github.com/mozilla/geckodriver/issues/846
45
- 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?
46
52
 
47
- native.click
48
53
  _send_keys(args).perform
49
54
  end
50
55
 
@@ -71,7 +76,7 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
71
76
  return super unless native_displayed?
72
77
 
73
78
  begin
74
- bridge.send(:execute, :is_element_displayed, id: native.ref)
79
+ bridge.send(:execute, :is_element_displayed, id: native_id)
75
80
  rescue Selenium::WebDriver::Error::UnknownCommandError
76
81
  # If the is_element_displayed command is unknown, no point in trying again
77
82
  driver.options[:native_displayed] = false
@@ -85,7 +90,7 @@ private
85
90
  (driver.options[:native_displayed] != false) && !ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS']
86
91
  end
87
92
 
88
- def click_with_options(click_options)
93
+ def perform_with_options(click_options)
89
94
  # Firefox/marionette has an issue clicking with offset near viewport edge
90
95
  # scroll element to middle just in case
91
96
  scroll_to_center if click_options.coords?
@@ -14,7 +14,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
14
14
  warn 'You are attempting to click a table row which has issues in safaridriver - '\
15
15
  'Your test should probably be clicking on a table cell like a user would. '\
16
16
  'Clicking the first cell in the row instead.'
17
- 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)
18
18
  end
19
19
  raise
20
20
  rescue ::Selenium::WebDriver::Error::WebDriverError => e
@@ -43,7 +43,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
43
43
  return '' unless visible?
44
44
 
45
45
  vis_text = driver.execute_script('return arguments[0].innerText', self)
46
- vis_text.gsub(/\ +/, ' ')
46
+ vis_text.squeeze(' ')
47
47
  .gsub(/[\ \n]*\n[\ \n]*/, "\n")
48
48
  .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
49
49
  .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
@@ -74,7 +74,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
74
74
  if clear == :backspace
75
75
  # Clear field by sending the correct number of backspace keys.
76
76
  backspaces = [:backspace] * self.value.to_s.length
77
- send_keys(*([[:control, 'e']] + backspaces + [value]))
77
+ send_keys([:control, 'e'], *backspaces, value)
78
78
  else
79
79
  super.tap do
80
80
  # React doesn't see the safaridriver element clear
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPauser
4
+ def initialize(mouse, keyboard)
5
+ super
6
+ @devices[:pauser] = Pauser.new
7
+ end
8
+
9
+ def pause(duration)
10
+ @actions << [:pauser, :pause, [duration]]
11
+ self
12
+ end
13
+
14
+ class Pauser
15
+ def pause(duration)
16
+ sleep duration
17
+ end
18
+ end
19
+
20
+ private_constant :Pauser
21
+ end
22
+
23
+ if defined?(::Selenium::WebDriver::VERSION) && (::Selenium::WebDriver::VERSION.to_f < 4) &&
24
+ defined?(::Selenium::WebDriver::ActionBuilder)
25
+ ::Selenium::WebDriver::ActionBuilder.prepend(ActionPauser)
26
+ end
@@ -6,10 +6,10 @@ private
6
6
  def read_atom(function)
7
7
  @atoms ||= Hash.new do |hash, key|
8
8
  hash[key] = begin
9
- File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
10
- rescue Errno::ENOENT
11
- super
12
- end
9
+ File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__))
10
+ rescue Errno::ENOENT
11
+ super
12
+ end
13
13
  end
14
14
  @atoms[function]
15
15
  end
@@ -27,17 +27,15 @@ module Capybara
27
27
 
28
28
  def log(type)
29
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
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
34
 
35
35
  Array(data).map do |l|
36
- begin
37
- ::Selenium::WebDriver::LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message')
38
- rescue KeyError
39
- next
40
- end
36
+ ::Selenium::WebDriver::LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message')
37
+ rescue KeyError
38
+ next
41
39
  end
42
40
  rescue ::Selenium::WebDriver::Error::UnknownCommandError
43
41
  raise NotImplementedError, LOG_MSG
@@ -16,16 +16,20 @@ 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 = format(DISABLE_JS_MARKUP_TEMPLATE,
22
+ selector: self.class.selector_for(Capybara.disable_animation))
20
23
  end
21
24
 
22
25
  def call(env)
23
26
  @status, @headers, @body = @app.call(env)
24
27
  return [@status, @headers, @body] unless html_content?
25
28
 
29
+ nonces = directive_nonces.transform_values { |nonce| "nonce=\"#{nonce}\"" if nonce && !nonce.empty? }
26
30
  response = Rack::Response.new([], @status, @headers)
27
31
 
28
- @body.each { |html| response.write insert_disable(html) }
32
+ @body.each { |html| response.write insert_disable(html, nonces) }
29
33
  @body.close if @body.respond_to?(:close)
30
34
 
31
35
  response.finish
@@ -33,26 +37,45 @@ module Capybara
33
37
 
34
38
  private
35
39
 
36
- attr_reader :disable_markup
40
+ attr_reader :disable_css_markup, :disable_js_markup
37
41
 
38
42
  def html_content?
39
43
  /html/.match?(@headers['Content-Type'])
40
44
  end
41
45
 
42
- def insert_disable(html)
43
- html.sub(%r{(</head>)}, disable_markup + '\\1')
46
+ def insert_disable(html, nonces)
47
+ html.sub(%r{(</head>)}, "<style #{nonces['style-src']}>#{disable_css_markup}</style>\\1")
48
+ .sub(%r{(</body>)}, "<script #{nonces['script-src']}>#{disable_js_markup}</script>\\1")
44
49
  end
45
50
 
46
- DISABLE_MARKUP_TEMPLATE = <<~HTML
47
- <script defer>(typeof jQuery !== 'undefined') && (jQuery.fx.off = true);</script>
48
- <style>
49
- %<selector>s, %<selector>s::before, %<selector>s::after {
50
- transition: none !important;
51
- animation-duration: 0s !important;
52
- animation-delay: 0s !important;
53
- }
54
- </style>
55
- HTML
51
+ def directive_nonces
52
+ @headers.fetch('Content-Security-Policy', '')
53
+ .split(';')
54
+ .map(&:split)
55
+ .to_h do |s|
56
+ [
57
+ s[0], s[1..].filter_map do |value|
58
+ /^'nonce-(?<nonce>.+)'/ =~ value
59
+ nonce
60
+ end[0]
61
+ ]
62
+ end
63
+ end
64
+
65
+ DISABLE_CSS_MARKUP_TEMPLATE = <<~CSS
66
+ %<selector>s, %<selector>s::before, %<selector>s::after {
67
+ transition: none !important;
68
+ animation-duration: 0s !important;
69
+ animation-delay: 0s !important;
70
+ scroll-behavior: auto !important;
71
+ }
72
+ CSS
73
+
74
+ DISABLE_JS_MARKUP_TEMPLATE = <<~SCRIPT
75
+ //<![CDATA[
76
+ (typeof jQuery !== 'undefined') && (jQuery.fx.off = true);
77
+ //]]>
78
+ SCRIPT
56
79
  end
57
80
  end
58
81
  end
@@ -29,7 +29,7 @@ module Capybara
29
29
  end
30
30
 
31
31
  def https_request(&block)
32
- make_request(ssl_options, &block)
32
+ make_request(**ssl_options, &block)
33
33
  end
34
34
 
35
35
  def make_request(**options, &block)
@@ -4,19 +4,25 @@ module Capybara
4
4
  class Server
5
5
  class Middleware
6
6
  class Counter
7
- attr_reader :value
8
-
9
7
  def initialize
10
- @value = 0
8
+ @value = []
11
9
  @mutex = Mutex.new
12
10
  end
13
11
 
14
- def increment
15
- @mutex.synchronize { @value += 1 }
12
+ def increment(uri)
13
+ @mutex.synchronize { @value.push(uri) }
14
+ end
15
+
16
+ def decrement(uri)
17
+ @mutex.synchronize { @value.delete_at(@value.index(uri) || @value.length) }
18
+ end
19
+
20
+ def positive?
21
+ @mutex.synchronize { @value.length.positive? }
16
22
  end
17
23
 
18
- def decrement
19
- @mutex.synchronize { @value -= 1 }
24
+ def value
25
+ @mutex.synchronize { @value.dup }
20
26
  end
21
27
  end
22
28
 
@@ -31,8 +37,12 @@ module Capybara
31
37
  @server_errors = server_errors
32
38
  end
33
39
 
40
+ def pending_requests
41
+ @counter.value
42
+ end
43
+
34
44
  def pending_requests?
35
- @counter.value.positive?
45
+ @counter.positive?
36
46
  end
37
47
 
38
48
  def clear_error
@@ -43,14 +53,16 @@ module Capybara
43
53
  if env['PATH_INFO'] == '/__identify__'
44
54
  [200, {}, [@app.object_id.to_s]]
45
55
  else
46
- @counter.increment
56
+ request_uri = env['REQUEST_URI']
57
+ @counter.increment(request_uri)
58
+
47
59
  begin
48
60
  @extended_app.call(env)
49
61
  rescue *@server_errors => e
50
62
  @error ||= e
51
63
  raise e
52
64
  ensure
53
- @counter.decrement
65
+ @counter.decrement(request_uri)
54
66
  end
55
67
  end
56
68
  end
@@ -24,7 +24,9 @@ module Capybara
24
24
  host: Capybara.server_host,
25
25
  reportable_errors: Capybara.server_errors,
26
26
  extra_middleware: [])
27
- warn 'Positional arguments, other than the application, to Server#new are deprecated, please use keyword arguments' unless deprecated_options.empty?
27
+ unless deprecated_options.empty?
28
+ warn 'Positional arguments, other than the application, to Server#new are deprecated, please use keyword arguments'
29
+ end
28
30
  @app = app
29
31
  @extra_middleware = extra_middleware
30
32
  @server_thread = nil # suppress warnings
@@ -61,7 +63,7 @@ module Capybara
61
63
  def wait_for_pending_requests
62
64
  timer = Capybara::Helpers.timer(expire_in: 60)
63
65
  while pending_requests?
64
- raise 'Requests did not finish in 60 seconds' if timer.expired?
66
+ raise "Requests did not finish in 60 seconds: #{middleware.pending_requests}" if timer.expired?
65
67
 
66
68
  sleep 0.01
67
69
  end
@@ -106,7 +108,17 @@ module Capybara
106
108
 
107
109
  def find_available_port(host)
108
110
  server = TCPServer.new(host, 0)
109
- server.addr[1]
111
+ port = server.addr[1]
112
+ server.close
113
+
114
+ # Workaround issue where some platforms (mac, ???) when passed a host
115
+ # of '0.0.0.0' will return a port that is only available on one of the
116
+ # ip addresses that resolves to, but the next binding to that port requires
117
+ # that port to be available on all ips
118
+ server = TCPServer.new(host, port)
119
+ port
120
+ rescue Errno::EADDRINUSE
121
+ retry
110
122
  ensure
111
123
  server&.close
112
124
  end
@@ -8,7 +8,7 @@ module Capybara
8
8
  automatic_reload match exact exact_text raise_server_errors visible_text_only
9
9
  automatic_label_click enable_aria_label save_path asset_host default_host app_host
10
10
  server_host server_port server_errors default_set_options disable_animation test_id
11
- predicates_wait default_normalize_ws w3c_click_offset].freeze
11
+ predicates_wait default_normalize_ws w3c_click_offset enable_aria_role].freeze
12
12
 
13
13
  attr_accessor(*OPTIONS)
14
14
 
@@ -37,6 +37,8 @@ module Capybara
37
37
  # See {Capybara.configure}
38
38
  # @!method enable_aria_label
39
39
  # See {Capybara.configure}
40
+ # @!method enable_aria_role
41
+ # See {Capybara.configure}
40
42
  # @!method save_path
41
43
  # See {Capybara.configure}
42
44
  # @!method asset_host
@@ -79,14 +81,18 @@ module Capybara
79
81
 
80
82
  remove_method :app_host=
81
83
  def app_host=(url)
82
- raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
84
+ unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
85
+ raise ArgumentError, "Capybara.app_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}."
86
+ end
83
87
 
84
88
  @app_host = url
85
89
  end
86
90
 
87
91
  remove_method :default_host=
88
92
  def default_host=(url)
89
- raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}." unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
93
+ unless url.nil? || url.match?(URI::DEFAULT_PARSER.make_regexp)
94
+ raise ArgumentError, "Capybara.default_host should be set to a url (http://www.example.com). Attempted to set #{url.inspect}."
95
+ end
90
96
 
91
97
  @default_host = url
92
98
  end
@@ -94,7 +100,7 @@ module Capybara
94
100
  remove_method :test_id=
95
101
  ##
96
102
  #
97
- # Set an attribue to be optionally matched against the locator for builtin selector types.
103
+ # Set an attribute to be optionally matched against the locator for builtin selector types.
98
104
  # This attribute will be checked by builtin selector types whenever id would normally be checked.
99
105
  # If `nil` then it will be ignored.
100
106
  #
@@ -13,14 +13,14 @@ module Capybara
13
13
  # @param string [String] The string that the current 'path' should equal
14
14
  # @overload $0(regexp, **options)
15
15
  # @param regexp [Regexp] The regexp that the current 'path' should match to
16
- # @option options [Boolean] :url (true if `string` ia a full url, otherwise false) Whether the compare should be done against the full current url or just the path
16
+ # @option options [Boolean] :url (true if `string` is a full url, otherwise false) Whether the comparison should be done against the full current url or just the path
17
17
  # @option options [Boolean] :ignore_query (false) Whether the query portion of the current url/path should be ignored
18
18
  # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current url/path to eq/match given string/regexp argument
19
19
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
20
20
  # @return [true]
21
21
  #
22
- def assert_current_path(path, **options)
23
- _verify_current_path(path, options) do |query|
22
+ def assert_current_path(path, **options, &optional_filter_block)
23
+ _verify_current_path(path, optional_filter_block, **options) do |query|
24
24
  raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
25
25
  end
26
26
  end
@@ -35,8 +35,8 @@ module Capybara
35
35
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
36
36
  # @return [true]
37
37
  #
38
- def assert_no_current_path(path, **options)
39
- _verify_current_path(path, options) do |query|
38
+ def assert_no_current_path(path, **options, &optional_filter_block)
39
+ _verify_current_path(path, optional_filter_block, **options) do |query|
40
40
  raise Capybara::ExpectationNotMet, query.negative_failure_message if query.resolves_for?(self)
41
41
  end
42
42
  end
@@ -50,8 +50,8 @@ module Capybara
50
50
  # @macro current_path_query_params
51
51
  # @return [Boolean]
52
52
  #
53
- def has_current_path?(path, **options)
54
- make_predicate(options) { assert_current_path(path, options) }
53
+ def has_current_path?(path, **options, &optional_filter_block)
54
+ make_predicate(options) { assert_current_path(path, **options, &optional_filter_block) }
55
55
  end
56
56
 
57
57
  ##
@@ -63,14 +63,14 @@ module Capybara
63
63
  # @macro current_path_query_params
64
64
  # @return [Boolean]
65
65
  #
66
- def has_no_current_path?(path, **options)
67
- make_predicate(options) { assert_no_current_path(path, options) }
66
+ def has_no_current_path?(path, **options, &optional_filter_block)
67
+ make_predicate(options) { assert_no_current_path(path, **options, &optional_filter_block) }
68
68
  end
69
69
 
70
70
  private
71
71
 
72
- def _verify_current_path(path, options)
73
- query = Capybara::Queries::CurrentPathQuery.new(path, options)
72
+ def _verify_current_path(path, filter_block, **options)
73
+ query = Capybara::Queries::CurrentPathQuery.new(path, **options, &filter_block)
74
74
  document.synchronize(query.wait) do
75
75
  yield(query)
76
76
  end