capybara 2.5.0 → 2.18.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 (205) hide show
  1. checksums.yaml +5 -5
  2. data/.yard/templates_custom/default/class/html/selectors.erb +38 -0
  3. data/.yard/templates_custom/default/class/html/setup.rb +17 -0
  4. data/.yard/yard_extensions.rb +78 -0
  5. data/.yardopts +1 -0
  6. data/History.md +413 -10
  7. data/License.txt +1 -1
  8. data/README.md +237 -130
  9. data/lib/capybara/config.rb +132 -0
  10. data/lib/capybara/cucumber.rb +3 -1
  11. data/lib/capybara/driver/base.rb +27 -6
  12. data/lib/capybara/driver/node.rb +14 -5
  13. data/lib/capybara/dsl.rb +2 -3
  14. data/lib/capybara/helpers.rb +13 -65
  15. data/lib/capybara/minitest/spec.rb +177 -0
  16. data/lib/capybara/minitest.rb +278 -0
  17. data/lib/capybara/node/actions.rb +180 -24
  18. data/lib/capybara/node/base.rb +17 -5
  19. data/lib/capybara/node/document.rb +5 -0
  20. data/lib/capybara/node/document_matchers.rb +15 -14
  21. data/lib/capybara/node/element.rb +55 -7
  22. data/lib/capybara/node/finders.rb +179 -67
  23. data/lib/capybara/node/matchers.rb +301 -105
  24. data/lib/capybara/node/simple.rb +15 -4
  25. data/lib/capybara/queries/ancestor_query.rb +25 -0
  26. data/lib/capybara/queries/base_query.rb +69 -3
  27. data/lib/capybara/queries/current_path_query.rb +17 -8
  28. data/lib/capybara/queries/match_query.rb +19 -0
  29. data/lib/capybara/queries/selector_query.rb +251 -0
  30. data/lib/capybara/queries/sibling_query.rb +25 -0
  31. data/lib/capybara/queries/text_query.rb +67 -16
  32. data/lib/capybara/queries/title_query.rb +4 -2
  33. data/lib/capybara/query.rb +3 -131
  34. data/lib/capybara/rack_test/browser.rb +14 -5
  35. data/lib/capybara/rack_test/css_handlers.rb +1 -0
  36. data/lib/capybara/rack_test/driver.rb +15 -8
  37. data/lib/capybara/rack_test/form.rb +34 -12
  38. data/lib/capybara/rack_test/node.rb +29 -12
  39. data/lib/capybara/rails.rb +3 -3
  40. data/lib/capybara/result.rb +104 -9
  41. data/lib/capybara/rspec/compound.rb +95 -0
  42. data/lib/capybara/rspec/features.rb +17 -6
  43. data/lib/capybara/rspec/matcher_proxies.rb +45 -0
  44. data/lib/capybara/rspec/matchers.rb +199 -80
  45. data/lib/capybara/rspec.rb +4 -2
  46. data/lib/capybara/selector/css.rb +30 -0
  47. data/lib/capybara/selector/filter.rb +20 -0
  48. data/lib/capybara/selector/filter_set.rb +74 -0
  49. data/lib/capybara/selector/filters/base.rb +33 -0
  50. data/lib/capybara/selector/filters/expression_filter.rb +40 -0
  51. data/lib/capybara/selector/filters/node_filter.rb +27 -0
  52. data/lib/capybara/selector/selector.rb +276 -0
  53. data/lib/capybara/selector.rb +452 -157
  54. data/lib/capybara/selenium/driver.rb +282 -81
  55. data/lib/capybara/selenium/node.rb +144 -46
  56. data/lib/capybara/server.rb +59 -16
  57. data/lib/capybara/session/config.rb +114 -0
  58. data/lib/capybara/session/matchers.rb +29 -19
  59. data/lib/capybara/session.rb +378 -143
  60. data/lib/capybara/spec/fixtures/no_extension +1 -0
  61. data/lib/capybara/spec/public/jquery-ui.js +13 -791
  62. data/lib/capybara/spec/public/jquery.js +4 -9045
  63. data/lib/capybara/spec/public/test.js +45 -11
  64. data/lib/capybara/spec/session/accept_alert_spec.rb +30 -7
  65. data/lib/capybara/spec/session/accept_confirm_spec.rb +14 -2
  66. data/lib/capybara/spec/session/accept_prompt_spec.rb +35 -6
  67. data/lib/capybara/spec/session/all_spec.rb +45 -32
  68. data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
  69. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +110 -0
  70. data/lib/capybara/spec/session/assert_current_path.rb +15 -2
  71. data/lib/capybara/spec/session/assert_selector.rb +29 -28
  72. data/lib/capybara/spec/session/assert_text.rb +59 -20
  73. data/lib/capybara/spec/session/assert_title.rb +25 -11
  74. data/lib/capybara/spec/session/attach_file_spec.rb +42 -4
  75. data/lib/capybara/spec/session/body_spec.rb +1 -0
  76. data/lib/capybara/spec/session/check_spec.rb +90 -14
  77. data/lib/capybara/spec/session/choose_spec.rb +31 -5
  78. data/lib/capybara/spec/session/click_button_spec.rb +20 -9
  79. data/lib/capybara/spec/session/click_link_or_button_spec.rb +15 -9
  80. data/lib/capybara/spec/session/click_link_spec.rb +39 -15
  81. data/lib/capybara/spec/session/current_scope_spec.rb +2 -1
  82. data/lib/capybara/spec/session/current_url_spec.rb +12 -3
  83. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +6 -5
  84. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +4 -3
  85. data/lib/capybara/spec/session/element/assert_match_selector.rb +36 -0
  86. data/lib/capybara/spec/session/element/match_css_spec.rb +23 -0
  87. data/lib/capybara/spec/session/element/match_xpath_spec.rb +23 -0
  88. data/lib/capybara/spec/session/element/matches_selector_spec.rb +106 -0
  89. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +22 -0
  90. data/lib/capybara/spec/session/evaluate_script_spec.rb +23 -1
  91. data/lib/capybara/spec/session/execute_script_spec.rb +22 -3
  92. data/lib/capybara/spec/session/fill_in_spec.rb +50 -32
  93. data/lib/capybara/spec/session/find_button_spec.rb +43 -2
  94. data/lib/capybara/spec/session/find_by_id_spec.rb +3 -2
  95. data/lib/capybara/spec/session/find_field_spec.rb +42 -6
  96. data/lib/capybara/spec/session/find_link_spec.rb +22 -3
  97. data/lib/capybara/spec/session/find_spec.rb +103 -57
  98. data/lib/capybara/spec/session/first_spec.rb +34 -18
  99. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +103 -0
  100. data/lib/capybara/spec/session/{within_frame_spec.rb → frame/within_frame_spec.rb} +44 -2
  101. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  102. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  103. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  104. data/lib/capybara/spec/session/has_button_spec.rb +17 -8
  105. data/lib/capybara/spec/session/has_css_spec.rb +85 -73
  106. data/lib/capybara/spec/session/has_current_path_spec.rb +91 -7
  107. data/lib/capybara/spec/session/has_field_spec.rb +93 -58
  108. data/lib/capybara/spec/session/has_link_spec.rb +9 -8
  109. data/lib/capybara/spec/session/has_none_selectors_spec.rb +76 -0
  110. data/lib/capybara/spec/session/has_select_spec.rb +159 -59
  111. data/lib/capybara/spec/session/has_selector_spec.rb +64 -28
  112. data/lib/capybara/spec/session/has_table_spec.rb +1 -0
  113. data/lib/capybara/spec/session/has_text_spec.rb +27 -12
  114. data/lib/capybara/spec/session/has_title_spec.rb +22 -4
  115. data/lib/capybara/spec/session/has_xpath_spec.rb +32 -29
  116. data/lib/capybara/spec/session/headers.rb +2 -1
  117. data/lib/capybara/spec/session/html_spec.rb +4 -3
  118. data/lib/capybara/spec/session/node_spec.rb +198 -38
  119. data/lib/capybara/spec/session/refresh_spec.rb +28 -0
  120. data/lib/capybara/spec/session/reset_session_spec.rb +46 -5
  121. data/lib/capybara/spec/session/response_code.rb +2 -1
  122. data/lib/capybara/spec/session/save_and_open_page_spec.rb +1 -0
  123. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +6 -5
  124. data/lib/capybara/spec/session/save_page_spec.rb +34 -2
  125. data/lib/capybara/spec/session/save_screenshot_spec.rb +31 -1
  126. data/lib/capybara/spec/session/screenshot_spec.rb +4 -2
  127. data/lib/capybara/spec/session/select_spec.rb +34 -32
  128. data/lib/capybara/spec/session/selectors_spec.rb +65 -0
  129. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  130. data/lib/capybara/spec/session/text_spec.rb +4 -4
  131. data/lib/capybara/spec/session/title_spec.rb +2 -1
  132. data/lib/capybara/spec/session/uncheck_spec.rb +42 -2
  133. data/lib/capybara/spec/session/unselect_spec.rb +17 -16
  134. data/lib/capybara/spec/session/visit_spec.rb +77 -2
  135. data/lib/capybara/spec/session/window/become_closed_spec.rb +12 -11
  136. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
  137. data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
  138. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +16 -11
  139. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +7 -4
  140. data/lib/capybara/spec/session/window/window_spec.rb +36 -29
  141. data/lib/capybara/spec/session/window/windows_spec.rb +1 -0
  142. data/lib/capybara/spec/session/window/within_window_spec.rb +31 -7
  143. data/lib/capybara/spec/session/within_spec.rb +14 -6
  144. data/lib/capybara/spec/spec_helper.rb +37 -4
  145. data/lib/capybara/spec/test_app.rb +15 -3
  146. data/lib/capybara/spec/views/buttons.erb +1 -0
  147. data/lib/capybara/spec/views/fieldsets.erb +2 -1
  148. data/lib/capybara/spec/views/form.erb +169 -9
  149. data/lib/capybara/spec/views/frame_child.erb +10 -2
  150. data/lib/capybara/spec/views/frame_one.erb +2 -1
  151. data/lib/capybara/spec/views/frame_parent.erb +3 -2
  152. data/lib/capybara/spec/views/frame_two.erb +2 -1
  153. data/lib/capybara/spec/views/header_links.erb +1 -0
  154. data/lib/capybara/spec/views/host_links.erb +1 -0
  155. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  156. data/lib/capybara/spec/views/path.erb +1 -0
  157. data/lib/capybara/spec/views/popup_one.erb +1 -0
  158. data/lib/capybara/spec/views/popup_two.erb +1 -0
  159. data/lib/capybara/spec/views/postback.erb +2 -1
  160. data/lib/capybara/spec/views/tables.erb +1 -0
  161. data/lib/capybara/spec/views/with_base_tag.erb +1 -0
  162. data/lib/capybara/spec/views/with_count.erb +2 -1
  163. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  164. data/lib/capybara/spec/views/with_hover.erb +7 -1
  165. data/lib/capybara/spec/views/with_html.erb +40 -2
  166. data/lib/capybara/spec/views/with_html_entities.erb +1 -0
  167. data/lib/capybara/spec/views/with_js.erb +32 -1
  168. data/lib/capybara/spec/views/with_scope.erb +1 -0
  169. data/lib/capybara/spec/views/with_simple_html.erb +2 -1
  170. data/lib/capybara/spec/views/with_slow_unload.erb +17 -0
  171. data/lib/capybara/spec/views/with_title.erb +2 -1
  172. data/lib/capybara/spec/views/with_unload_alert.erb +14 -0
  173. data/lib/capybara/spec/views/with_windows.erb +7 -0
  174. data/lib/capybara/spec/views/within_frames.erb +3 -2
  175. data/lib/capybara/version.rb +2 -1
  176. data/lib/capybara/window.rb +20 -3
  177. data/lib/capybara.rb +189 -93
  178. data/spec/basic_node_spec.rb +7 -6
  179. data/spec/capybara_spec.rb +90 -4
  180. data/spec/dsl_spec.rb +3 -1
  181. data/spec/filter_set_spec.rb +28 -0
  182. data/spec/fixtures/capybara.csv +1 -0
  183. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -1
  184. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -1
  185. data/spec/minitest_spec.rb +130 -0
  186. data/spec/minitest_spec_spec.rb +135 -0
  187. data/spec/per_session_config_spec.rb +67 -0
  188. data/spec/rack_test_spec.rb +50 -7
  189. data/spec/result_spec.rb +76 -0
  190. data/spec/rspec/features_spec.rb +21 -8
  191. data/spec/rspec/scenarios_spec.rb +21 -0
  192. data/spec/rspec/{matchers_spec.rb → shared_spec_matchers.rb} +160 -54
  193. data/spec/rspec/views_spec.rb +5 -0
  194. data/spec/rspec_matchers_spec.rb +46 -0
  195. data/spec/rspec_spec.rb +79 -1
  196. data/spec/selector_spec.rb +199 -0
  197. data/spec/selenium_spec_chrome.rb +54 -9
  198. data/spec/selenium_spec_firefox.rb +68 -0
  199. data/spec/selenium_spec_marionette.rb +127 -0
  200. data/spec/server_spec.rb +102 -14
  201. data/spec/session_spec.rb +54 -0
  202. data/spec/shared_selenium_session.rb +215 -0
  203. data/spec/spec_helper.rb +7 -0
  204. metadata +140 -15
  205. data/spec/selenium_spec.rb +0 -128
@@ -1,17 +1,29 @@
1
+ # frozen_string_literal: true
1
2
  require "uri"
2
3
 
3
4
  class Capybara::Selenium::Driver < Capybara::Driver::Base
5
+
4
6
  DEFAULT_OPTIONS = {
5
- :browser => :firefox
7
+ :browser => :firefox,
8
+ clear_local_storage: false,
9
+ clear_session_storage: false
6
10
  }
7
- SPECIAL_OPTIONS = [:browser]
11
+ SPECIAL_OPTIONS = [:browser, :clear_local_storage, :clear_session_storage]
8
12
 
9
13
  attr_reader :app, :options
10
14
 
11
15
  def browser
12
16
  unless @browser
13
- @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,val| SPECIAL_OPTIONS.include?(key) })
17
+ if firefox?
18
+ options[:desired_capabilities] ||= {}
19
+ options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" })
20
+ end
14
21
 
22
+ @processed_options = options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) }
23
+ @browser = Selenium::WebDriver.for(options[:browser], @processed_options)
24
+
25
+ @w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
26
+ (defined?(Selenium::WebDriver::Remote::W3C::Capabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3C::Capabilities)))
15
27
  main = Process.pid
16
28
  at_exit do
17
29
  # Store the exit status of the test run since it goes away after calling the at_exit proc...
@@ -24,16 +36,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
24
36
  end
25
37
 
26
38
  def initialize(app, options={})
27
- begin
28
- require 'selenium-webdriver'
29
- rescue LoadError => e
30
- if e.message =~ /selenium-webdriver/
31
- raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
32
- else
33
- raise e
34
- end
35
- end
36
-
39
+ load_selenium
40
+ @session = nil
37
41
  @app = app
38
42
  @browser = nil
39
43
  @exit_status = nil
@@ -45,6 +49,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
45
49
  browser.navigate.to(path)
46
50
  end
47
51
 
52
+ def refresh
53
+ accept_modal(nil, wait: 0.1) do
54
+ browser.navigate.refresh
55
+ end
56
+ rescue Capybara::ModalNotFound
57
+ end
58
+
48
59
  def go_back
49
60
  browser.navigate.back
50
61
  end
@@ -76,36 +87,72 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
76
87
  def wait?; true; end
77
88
  def needs_server?; true; end
78
89
 
79
- def execute_script(script)
80
- browser.execute_script script
90
+ def execute_script(script, *args)
91
+ browser.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
81
92
  end
82
93
 
83
- def evaluate_script(script)
84
- browser.execute_script "return #{script}"
94
+ def evaluate_script(script, *args)
95
+ result = execute_script("return #{script}", *args)
96
+ unwrap_script_result(result)
85
97
  end
86
98
 
87
- def save_screenshot(path, options={})
99
+ def evaluate_async_script(script, *args)
100
+ browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
101
+ result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg} )
102
+ unwrap_script_result(result)
103
+ end
104
+
105
+ def save_screenshot(path, _options={})
88
106
  browser.save_screenshot(path)
89
107
  end
90
108
 
91
109
  def reset!
92
110
  # Use instance variable directly so we avoid starting the browser just to reset the session
93
111
  if @browser
112
+ navigated = false
113
+ start_time = Capybara::Helpers.monotonic_time
94
114
  begin
95
- begin @browser.manage.delete_all_cookies
96
- rescue Selenium::WebDriver::Error::UnhandledError
97
- # delete_all_cookies fails when we've previously gone
98
- # to about:blank, so we rescue this error and do nothing
99
- # instead.
115
+ if !navigated
116
+ # Only trigger a navigation if we haven't done it already, otherwise it
117
+ # can trigger an endless series of unload modals
118
+ begin
119
+ @browser.manage.delete_all_cookies
120
+ if options[:clear_session_storage]
121
+ if @browser.respond_to? :session_storage
122
+ @browser.session_storage.clear
123
+ else
124
+ warn "sessionStorage clear requested but is not available for this driver"
125
+ end
126
+ end
127
+ if options[:clear_local_storage]
128
+ if @browser.respond_to? :local_storage
129
+ @browser.local_storage.clear
130
+ else
131
+ warn "localStorage clear requested but is not available for this driver"
132
+ end
133
+ end
134
+ rescue Selenium::WebDriver::Error::UnhandledError
135
+ # delete_all_cookies fails when we've previously gone
136
+ # to about:blank, so we rescue this error and do nothing
137
+ # instead.
138
+ end
139
+ @browser.navigate.to("about:blank")
140
+ end
141
+ navigated = true
142
+
143
+ #Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
144
+ until find_xpath("/html/body/*").empty? do
145
+ raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
146
+ sleep 0.05
100
147
  end
101
- @browser.navigate.to("about:blank")
102
- rescue Selenium::WebDriver::Error::UnhandledAlertError
148
+ rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
103
149
  # This error is thrown if an unhandled alert is on the page
104
150
  # Firefox appears to automatically dismiss this alert, chrome does not
105
151
  # We'll try to accept it
106
152
  begin
107
153
  @browser.switch_to.alert.accept
108
- rescue Selenium::WebDriver::Error::NoAlertPresentError
154
+ sleep 0.25 # allow time for the modal to be handled
155
+ rescue modal_error
109
156
  # The alert is now gone - nothing to do
110
157
  end
111
158
  # try cleaning up the browser again
@@ -114,35 +161,21 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
114
161
  end
115
162
  end
116
163
 
117
- ##
118
- #
119
- # Webdriver supports frame name, id, index(zero-based) or {Capybara::Node::Element} to find iframe
120
- #
121
- # @overload within_frame(index)
122
- # @param [Integer] index index of a frame
123
- # @overload within_frame(name_or_id)
124
- # @param [String] name_or_id name or id of a frame
125
- # @overload within_frame(element)
126
- # @param [Capybara::Node::Base] a_node frame element
127
- #
128
- def within_frame(frame_handle)
129
- frame_handle = frame_handle.native if frame_handle.is_a?(Capybara::Node::Base)
130
- if !browser.switch_to.respond_to?(:parent_frame)
131
- # Selenium Webdriver < 2.43 doesnt support moving back to the parent
132
- @frame_handles[browser.window_handle] ||= []
133
- @frame_handles[browser.window_handle] << frame_handle
134
- end
135
- browser.switch_to.frame(frame_handle)
136
- yield
137
- ensure
138
- if browser.switch_to.respond_to?(:parent_frame)
139
- browser.switch_to.parent_frame
140
- else
141
- # There doesnt appear to be any way in Selenium Webdriver < 2.43 to move back to a parent frame
142
- # other than going back to the root and then reiterating down
164
+ def switch_to_frame(frame)
165
+ case frame
166
+ when :top
167
+ @frame_handles[browser.window_handle] = []
168
+ browser.switch_to.default_content
169
+ when :parent
170
+ # would love to use browser.switch_to.parent_frame here
171
+ # but it has an issue if the current frame is removed from within it
143
172
  @frame_handles[browser.window_handle].pop
144
173
  browser.switch_to.default_content
145
174
  @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
175
+ else
176
+ @frame_handles[browser.window_handle] ||= []
177
+ @frame_handles[browser.window_handle] << frame.native
178
+ browser.switch_to.frame(frame.native)
146
179
  end
147
180
  end
148
181
 
@@ -159,7 +192,12 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
159
192
 
160
193
  def resize_window_to(handle, width, height)
161
194
  within_given_window(handle) do
162
- browser.manage.window.resize_to(width, height)
195
+ # Don't set the size if already set - See https://github.com/mozilla/geckodriver/issues/643
196
+ if marionette? && (window_size(handle) == [width, height])
197
+ {}
198
+ else
199
+ browser.manage.window.resize_to(width, height)
200
+ end
163
201
  end
164
202
  end
165
203
 
@@ -188,39 +226,23 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
188
226
  browser.switch_to.window handle
189
227
  end
190
228
 
191
- # @api private
192
- def find_window(locator)
193
- handles = browser.window_handles
194
- return locator if handles.include? locator
195
-
196
- original_handle = browser.window_handle
197
- handles.each do |handle|
198
- switch_to_window(handle)
199
- if (locator == browser.execute_script("return window.name") ||
200
- browser.title.include?(locator) ||
201
- browser.current_url.include?(locator))
202
- switch_to_window(original_handle)
203
- return handle
204
- end
205
- end
206
- raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
207
- end
208
-
209
229
  def within_window(locator)
210
230
  handle = find_window(locator)
211
231
  browser.switch_to.window(handle) { yield }
212
232
  end
213
233
 
214
- def accept_modal(type, options={}, &blk)
234
+ def accept_modal(_type, options={})
215
235
  yield if block_given?
216
236
  modal = find_modal(options)
237
+
217
238
  modal.send_keys options[:with] if options[:with]
239
+
218
240
  message = modal.text
219
241
  modal.accept
220
242
  message
221
243
  end
222
244
 
223
- def dismiss_modal(type, options={}, &blk)
245
+ def dismiss_modal(_type, options={})
224
246
  yield if block_given?
225
247
  modal = find_modal(options)
226
248
  message = modal.text
@@ -230,29 +252,139 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
230
252
 
231
253
  def quit
232
254
  @browser.quit if @browser
233
- rescue Errno::ECONNREFUSED
255
+ rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
234
256
  # Browser must have already gone
257
+ rescue Selenium::WebDriver::Error::UnknownError => e
258
+ unless silenced_unknown_error_message?(e.message) # Most likely already gone
259
+ # probably already gone but not sure - so warn
260
+ warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
261
+ end
235
262
  ensure
236
263
  @browser = nil
237
264
  end
238
265
 
239
266
  def invalid_element_errors
240
- [Selenium::WebDriver::Error::StaleElementReferenceError,
241
- Selenium::WebDriver::Error::UnhandledError,
242
- Selenium::WebDriver::Error::ElementNotVisibleError,
243
- Selenium::WebDriver::Error::InvalidSelectorError] # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
267
+ [::Selenium::WebDriver::Error::StaleElementReferenceError,
268
+ ::Selenium::WebDriver::Error::UnhandledError,
269
+ ::Selenium::WebDriver::Error::ElementNotVisibleError,
270
+ ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
271
+ ::Selenium::WebDriver::Error::ElementNotInteractableError,
272
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
273
+ ::Selenium::WebDriver::Error::InvalidElementStateError,
274
+ ::Selenium::WebDriver::Error::ElementNotSelectableError,
275
+ ]
244
276
  end
245
277
 
246
278
  def no_such_window_error
247
279
  Selenium::WebDriver::Error::NoSuchWindowError
248
280
  end
249
281
 
282
+ # @api private
283
+ def marionette?
284
+ firefox? && browser && @w3c
285
+ end
286
+
287
+ # @api private
288
+ def firefox?
289
+ browser_name == "firefox"
290
+ end
291
+
292
+ # @api private
293
+ def chrome?
294
+ browser_name == "chrome"
295
+ end
296
+
297
+ # @deprecated This method is being removed
250
298
  def browser_initialized?
251
- !@browser.nil?
299
+ super && !@browser.nil?
252
300
  end
253
301
 
254
302
  private
255
303
 
304
+ # @api private
305
+ def browser_name
306
+ options[:browser].to_s
307
+ end
308
+
309
+ def modal_error
310
+ if defined?(Selenium::WebDriver::Error::NoSuchAlertError)
311
+ Selenium::WebDriver::Error::NoSuchAlertError
312
+ else
313
+ Selenium::WebDriver::Error::NoAlertPresentError
314
+ end
315
+ end
316
+
317
+ def find_window(locator)
318
+ handles = browser.window_handles
319
+ return locator if handles.include? locator
320
+
321
+ original_handle = browser.window_handle
322
+ handles.each do |handle|
323
+ switch_to_window(handle)
324
+ if (locator == browser.execute_script("return window.name") ||
325
+ browser.title.include?(locator) ||
326
+ browser.current_url.include?(locator))
327
+ switch_to_window(original_handle)
328
+ return handle
329
+ end
330
+ end
331
+ raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
332
+ end
333
+
334
+ def insert_modal_handlers(accept, response_text)
335
+ prompt_response = if accept
336
+ if response_text.nil?
337
+ "default_text"
338
+ else
339
+ "'#{response_text.gsub("\\", "\\\\\\").gsub("'", "\\\\'")}'"
340
+ end
341
+ else
342
+ 'null'
343
+ end
344
+
345
+ script = <<-JS
346
+ if (typeof window.capybara === 'undefined') {
347
+ window.capybara = {
348
+ modal_handlers: [],
349
+ current_modal_status: function() {
350
+ return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text];
351
+ },
352
+ add_handler: function(handler) {
353
+ this.modal_handlers.unshift(handler);
354
+ },
355
+ remove_handler: function(handler) {
356
+ window.alert = handler.alert;
357
+ window.confirm = handler.confirm;
358
+ window.prompt = handler.prompt;
359
+ },
360
+ handler_called: function(handler, str) {
361
+ handler.called = true;
362
+ handler.modal_text = str;
363
+ this.remove_handler(handler);
364
+ }
365
+ };
366
+ };
367
+
368
+ var modal_handler = {
369
+ prompt: window.prompt,
370
+ confirm: window.confirm,
371
+ alert: window.alert,
372
+ called: false
373
+ }
374
+ window.capybara.add_handler(modal_handler);
375
+
376
+ window.alert = window.confirm = function(str = "") {
377
+ window.capybara.handler_called(modal_handler, str.toString());
378
+ return #{accept ? 'true' : 'false'};
379
+ }
380
+ window.prompt = function(str = "", default_text = "") {
381
+ window.capybara.handler_called(modal_handler, str.toString());
382
+ return #{prompt_response};
383
+ }
384
+ JS
385
+ execute_script script
386
+ end
387
+
256
388
  def within_given_window(handle)
257
389
  original_handle = self.current_window_handle
258
390
  if handle == original_handle
@@ -269,8 +401,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
269
401
  # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
270
402
  # Actual wait time may be longer than specified
271
403
  wait = Selenium::WebDriver::Wait.new(
272
- timeout: (options[:wait] || Capybara.default_max_wait_time),
273
- ignore: Selenium::WebDriver::Error::NoAlertPresentError)
404
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
405
+ ignore: modal_error)
274
406
  begin
275
407
  wait.until do
276
408
  alert = @browser.switch_to.alert
@@ -282,4 +414,73 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
282
414
  end
283
415
  end
284
416
 
417
+ def find_headless_modal(options={})
418
+ # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
419
+ # Actual wait time may be longer than specified
420
+ wait = Selenium::WebDriver::Wait.new(
421
+ timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 ,
422
+ ignore: modal_error)
423
+ begin
424
+ wait.until do
425
+ called, alert_text = evaluate_script('window.capybara && window.capybara.current_modal_status()')
426
+ if called
427
+ execute_script('window.capybara && window.capybara.modal_handlers.shift()')
428
+ regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
429
+ if alert_text.match(regexp)
430
+ alert_text
431
+ else
432
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
433
+ end
434
+ elsif called.nil?
435
+ # page changed so modal_handler data has gone away
436
+ warn "Can't verify modal text when page change occurs - ignoring" if options[:text]
437
+ ""
438
+ else
439
+ nil
440
+ end
441
+ end
442
+ rescue Selenium::WebDriver::Error::TimeOutError
443
+ raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
444
+ end
445
+ end
446
+
447
+ def silenced_unknown_error_message?(msg)
448
+ silenced_unknown_error_messages.any? { |r| msg =~ r }
449
+ end
450
+
451
+ def silenced_unknown_error_messages
452
+ [ /Error communicating with the remote browser/ ]
453
+ end
454
+
455
+ def unwrap_script_result(arg)
456
+ case arg
457
+ when Array
458
+ arg.map { |e| unwrap_script_result(e) }
459
+ when Hash
460
+ arg.each { |k, v| arg[k] = unwrap_script_result(v) }
461
+ when Selenium::WebDriver::Element
462
+ Capybara::Selenium::Node.new(self, arg)
463
+ else
464
+ arg
465
+ end
466
+ end
467
+
468
+ def load_selenium
469
+ begin
470
+ require 'selenium-webdriver'
471
+ # Fix for selenium-webdriver 3.4.0 which misnamed these
472
+ if !defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
473
+ ::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
474
+ end
475
+ if !defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
476
+ ::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
477
+ end
478
+ rescue LoadError => e
479
+ if e.message =~ /selenium-webdriver/
480
+ raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
481
+ else
482
+ raise e
483
+ end
484
+ end
485
+ end
285
486
  end