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,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'capybara/session/matchers'
3
+ require 'addressable/uri'
2
4
 
3
5
  module Capybara
4
6
 
@@ -15,11 +17,19 @@ module Capybara
15
17
  # session = Capybara::Session.new(:culerity)
16
18
  # session.visit('http://www.google.com')
17
19
  #
20
+ # When Capybara.threadsafe == true the sessions options will be initially set to the
21
+ # current values of the global options and a configuration block can be passed to the session initializer.
22
+ # For available options see {Capybara::SessionConfig::OPTIONS}
23
+ #
24
+ # session = Capybara::Session.new(:driver, MyRackApp) do |config|
25
+ # config.app_host = "http://my_host.dev"
26
+ # end
27
+ #
18
28
  # Session provides a number of methods for controlling the navigation of the page, such as +visit+,
19
29
  # +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
20
30
  # the current HTML document. This allows interaction:
21
31
  #
22
- # session.fill_in('q', :with => 'Capybara')
32
+ # session.fill_in('q', with: 'Capybara')
23
33
  # session.click_button('Search')
24
34
  # expect(session).to have_content('Capybara')
25
35
  #
@@ -39,6 +49,7 @@ module Capybara
39
49
  :has_no_table?, :has_table?, :unselect, :has_select?, :has_no_select?,
40
50
  :has_selector?, :has_no_selector?, :click_on, :has_no_checked_field?,
41
51
  :has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector,
52
+ :assert_all_of_selectors, :assert_none_of_selectors,
42
53
  :refute_selector, :assert_text, :assert_no_text
43
54
  ]
44
55
  # @api private
@@ -47,9 +58,9 @@ module Capybara
47
58
  ]
48
59
  SESSION_METHODS = [
49
60
  :body, :html, :source, :current_url, :current_host, :current_path,
50
- :execute_script, :evaluate_script, :visit, :go_back, :go_forward,
51
- :within, :within_fieldset, :within_table, :within_frame, :current_window,
52
- :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
61
+ :execute_script, :evaluate_script, :visit, :refresh, :go_back, :go_forward,
62
+ :within, :within_element, :within_fieldset, :within_table, :within_frame, :switch_to_frame,
63
+ :current_window, :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
53
64
  :save_page, :save_and_open_page, :save_screenshot,
54
65
  :save_and_open_screenshot, :reset_session!, :response_headers,
55
66
  :status_code, :current_scope,
@@ -65,10 +76,16 @@ module Capybara
65
76
  attr_accessor :synchronized
66
77
 
67
78
  def initialize(mode, app=nil)
79
+ raise TypeError, "The second parameter to Session::new should be a rack app if passed." if app && !app.respond_to?(:call)
80
+ @@instance_created = true
68
81
  @mode = mode
69
82
  @app = app
70
- if Capybara.run_server and @app and driver.needs_server?
71
- @server = Capybara::Server.new(@app).boot
83
+ if block_given?
84
+ raise "A configuration block is only accepted when Capybara.threadsafe == true" unless Capybara.threadsafe
85
+ yield config if block_given?
86
+ end
87
+ if config.run_server and @app and driver.needs_server?
88
+ @server = Capybara::Server.new(@app, config.server_port, config.server_host, config.server_errors).boot
72
89
  else
73
90
  @server = nil
74
91
  end
@@ -81,7 +98,9 @@ module Capybara
81
98
  other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
82
99
  raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
83
100
  end
84
- Capybara.drivers[mode].call(app)
101
+ driver = Capybara.drivers[mode].call(app)
102
+ driver.session = self if driver.respond_to?(:session=)
103
+ driver
85
104
  end
86
105
  end
87
106
 
@@ -106,9 +125,9 @@ module Capybara
106
125
  def reset!
107
126
  if @touched
108
127
  driver.reset!
109
- assert_no_selector :xpath, "/html/body/*" if driver.browser_initialized?
110
128
  @touched = false
111
129
  end
130
+ @server.wait_for_pending_requests if @server
112
131
  raise_server_error!
113
132
  end
114
133
  alias_method :cleanup!, :reset!
@@ -119,9 +138,19 @@ module Capybara
119
138
  # Raise errors encountered in the server
120
139
  #
121
140
  def raise_server_error!
122
- raise @server.error if Capybara.raise_server_errors and @server and @server.error
123
- ensure
124
- @server.reset_error! if @server
141
+ if @server and @server.error
142
+ # Force an explanation for the error being raised as the exception cause
143
+ begin
144
+ if config.raise_server_errors
145
+ raise CapybaraError, "Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true"
146
+ end
147
+ rescue CapybaraError
148
+ #needed to get the cause set correctly in JRuby -- otherwise we could just do raise @server.error
149
+ raise @server.error, @server.error.message, @server.error.backtrace
150
+ ensure
151
+ @server.reset_error!
152
+ end
153
+ end
125
154
  end
126
155
 
127
156
  ##
@@ -159,7 +188,15 @@ module Capybara
159
188
  # @return [String] Path of the current page, without any domain information
160
189
  #
161
190
  def current_path
162
- path = URI.parse(current_url).path
191
+ # Addressable parsing is more lenient than URI
192
+ uri = ::Addressable::URI.parse(current_url)
193
+
194
+ # If current_url ends up being nil, won't be able to call .path on a NilClass.
195
+ return nil if uri.nil?
196
+
197
+ # Addressable doesn't support opaque URIs - we want nil here
198
+ return nil if uri.scheme == "about"
199
+ path = uri.path
163
200
  path if path and not path.empty?
164
201
  end
165
202
 
@@ -204,32 +241,46 @@ module Capybara
204
241
  #
205
242
  # Will actually navigate to `http://google.com:4567/test`.
206
243
  #
207
- # @param [#to_s] url The URL to navigate to. The parameter will be cast to a String.
244
+ # @param [#to_s] visit_uri The URL to navigate to. The parameter will be cast to a String.
208
245
  #
209
- def visit(url)
246
+ def visit(visit_uri)
210
247
  raise_server_error!
211
-
212
- url = url.to_s
213
248
  @touched = true
214
249
 
215
- url_relative = URI.parse(url).scheme.nil?
250
+ visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
216
251
 
217
- if url_relative && Capybara.app_host
218
- url = Capybara.app_host + url
219
- url_relative = false
252
+ uri_base = if @server
253
+ ::Addressable::URI.parse(config.app_host || "http://#{@server.host}:#{@server.port}")
254
+ else
255
+ config.app_host && ::Addressable::URI.parse(config.app_host)
220
256
  end
221
257
 
222
- if @server
223
- url = "http://#{@server.host}:#{@server.port}" + url if url_relative
258
+ if uri_base && [nil, 'http', 'https'].include?(visit_uri.scheme)
259
+ if visit_uri.relative?
260
+ uri_base.port ||= @server.port if @server && config.always_include_port
261
+
262
+ visit_uri_parts = visit_uri.to_hash.delete_if { |k,v| v.nil? }
263
+
264
+ # Useful to people deploying to a subdirectory
265
+ # and/or single page apps where only the url fragment changes
266
+ visit_uri_parts[:path] = uri_base.path + visit_uri.path
224
267
 
225
- if Capybara.always_include_port
226
- uri = URI.parse(url)
227
- uri.port = @server.port if uri.port == uri.default_port
228
- url = uri.to_s
268
+ visit_uri = uri_base.merge(visit_uri_parts)
269
+ else
270
+ visit_uri.port ||= @server.port if @server && config.always_include_port
229
271
  end
230
272
  end
231
273
 
232
- driver.visit(url)
274
+ driver.visit(visit_uri.to_s)
275
+ end
276
+
277
+ ##
278
+ #
279
+ # Refresh the page
280
+ #
281
+ def refresh
282
+ raise_server_error!
283
+ driver.refresh
233
284
  end
234
285
 
235
286
  ##
@@ -255,8 +306,8 @@ module Capybara
255
306
  # block, any command to Capybara will be handled as though it were scoped
256
307
  # to the given element.
257
308
  #
258
- # within(:xpath, '//div[@id="delivery-address"]') do
259
- # fill_in('Street', :with => '12 Main Street')
309
+ # within(:xpath, './/div[@id="delivery-address"]') do
310
+ # fill_in('Street', with: '12 Main Street')
260
311
  # end
261
312
  #
262
313
  # Just as with `find`, if multiple elements match the selector given to
@@ -267,13 +318,13 @@ module Capybara
267
318
  # assumed to be of the type set in Capybara.default_selector.
268
319
  #
269
320
  # within('div#delivery-address') do
270
- # fill_in('Street', :with => '12 Main Street')
321
+ # fill_in('Street', with: '12 Main Street')
271
322
  # end
272
323
  #
273
324
  # Note that a lot of uses of `within` can be replaced more succinctly with
274
325
  # chaining:
275
326
  #
276
- # find('div#delivery-address').fill_in('Street', :with => '12 Main Street')
327
+ # find('div#delivery-address').fill_in('Street', with: '12 Main Street')
277
328
  #
278
329
  # @overload within(*find_args)
279
330
  # @param (see Capybara::Node::Finders#all)
@@ -292,6 +343,7 @@ module Capybara
292
343
  scopes.pop
293
344
  end
294
345
  end
346
+ alias_method :within_element, :within
295
347
 
296
348
  ##
297
349
  #
@@ -319,21 +371,77 @@ module Capybara
319
371
 
320
372
  ##
321
373
  #
322
- # Execute the given block within the given iframe using given frame name or index.
323
- # May be supported by not all drivers. Drivers that support it, may provide additional options.
374
+ # Switch to the given frame
324
375
  #
325
- # @overload within_frame(index)
326
- # @param [Integer] index index of a frame
327
- # @overload within_frame(name)
328
- # @param [String] name name of a frame
376
+ # If you use this method you are responsible for making sure you switch back to the parent frame when done in the frame changed to.
377
+ # Capybara::Session#within_frame is preferred over this method and should be used when possible.
378
+ # May not be supported by all drivers.
329
379
  #
330
- def within_frame(frame_handle)
331
- scopes.push(nil)
332
- driver.within_frame(frame_handle) do
333
- yield
380
+ # @overload switch_to_frame(element)
381
+ # @param [Capybara::Node::Element] iframe/frame element to switch to
382
+ # @overload switch_to_frame(:parent)
383
+ # Switch to the parent element
384
+ # @overload switch_to_frame(:top)
385
+ # Switch to the top level document
386
+ #
387
+ def switch_to_frame(frame)
388
+ case frame
389
+ when Capybara::Node::Element
390
+ driver.switch_to_frame(frame)
391
+ scopes.push(:frame)
392
+ when :parent
393
+ raise Capybara::ScopeError, "`switch_to_frame(:parent)` cannot be called from inside a descendant frame's "\
394
+ "`within` block." if scopes.last() != :frame
395
+ scopes.pop
396
+ driver.switch_to_frame(:parent)
397
+ when :top
398
+ idx = scopes.index(:frame)
399
+ if idx
400
+ raise Capybara::ScopeError, "`switch_to_frame(:top)` cannot be called from inside a descendant frame's "\
401
+ "`within` block." if scopes.slice(idx..-1).any? {|scope| ![:frame, nil].include?(scope)}
402
+ scopes.slice!(idx..-1)
403
+ driver.switch_to_frame(:top)
404
+ end
405
+ end
406
+ end
407
+
408
+ ##
409
+ #
410
+ # Execute the given block within the given iframe using given frame, frame name/id or index.
411
+ # May not be supported by all drivers.
412
+ #
413
+ # @overload within_frame(element)
414
+ # @param [Capybara::Node::Element] frame element
415
+ # @overload within_frame([kind = :frame], locator, options = {})
416
+ # @param [Symobl] kind Optional selector type (:css, :xpath, :field, etc.) - Defaults to :frame
417
+ # @param [String] locator The locator for the given selector kind. For :frame this is the name/id of a frame/iframe element
418
+ # @overload within_frame(index)
419
+ # @param [Integer] index index of a frame (0 based)
420
+ def within_frame(*args)
421
+ frame = _find_frame(*args)
422
+
423
+ begin
424
+ switch_to_frame(frame)
425
+ begin
426
+ yield
427
+ ensure
428
+ switch_to_frame(:parent)
429
+ end
430
+ rescue Capybara::NotSupportedByDriverError
431
+ # Support older driver frame API for now
432
+ if driver.respond_to?(:within_frame)
433
+ begin
434
+ scopes.push(:frame)
435
+ driver.within_frame(frame) do
436
+ yield
437
+ end
438
+ ensure
439
+ scopes.pop
440
+ end
441
+ else
442
+ raise
443
+ end
334
444
  end
335
- ensure
336
- scopes.pop
337
445
  end
338
446
 
339
447
  ##
@@ -378,51 +486,27 @@ module Capybara
378
486
  # @raise [Capybara::WindowError] if no window matches given block
379
487
  # @overload switch_to_window(window)
380
488
  # @param window [Capybara::Window] window that should be switched to
381
- # @raise [Capybara::Driver::Base#no_such_window_error] if unexistent (e.g. closed) window was passed
489
+ # @raise [Capybara::Driver::Base#no_such_window_error] if nonexistent (e.g. closed) window was passed
382
490
  #
383
491
  # @return [Capybara::Window] window that has been switched to
384
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
385
- # `within_frame` or `within_window` methods
492
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within` or
493
+ # `within_frame` methods
386
494
  # @raise [ArgumentError] if both or neither arguments were provided
387
495
  #
388
- def switch_to_window(window = nil, options= {})
389
- if window.is_a? Hash
390
- options = window
391
- window = nil
392
- end
496
+ def switch_to_window(window = nil, options= {}, &window_locator)
497
+ options, window = window, nil if window.is_a? Hash
498
+
393
499
  block_given = block_given?
394
500
  if window && block_given
395
501
  raise ArgumentError, "`switch_to_window` can take either a block or a window, not both"
396
502
  elsif !window && !block_given
397
503
  raise ArgumentError, "`switch_to_window`: either window or block should be provided"
398
- elsif scopes.size > 1
504
+ elsif !scopes.last.nil?
399
505
  raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\
400
- "`within`'s, `within_frame`'s' or `within_window`'s' block."
506
+ "`within` or `within_frame` blocks."
401
507
  end
402
508
 
403
- if window
404
- driver.switch_to_window(window.handle)
405
- window
406
- else
407
- wait_time = Capybara::Query.new(options).wait
408
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
409
- original_window_handle = driver.current_window_handle
410
- begin
411
- driver.window_handles.each do |handle|
412
- driver.switch_to_window handle
413
- if yield
414
- return Window.new(self, handle)
415
- end
416
- end
417
- rescue => e
418
- driver.switch_to_window(original_window_handle)
419
- raise e
420
- else
421
- driver.switch_to_window(original_window_handle)
422
- raise Capybara::WindowError, "Could not find a window matching block/lambda"
423
- end
424
- end
425
- end
509
+ _switch_to_window(window, options, &window_locator)
426
510
  end
427
511
 
428
512
  ##
@@ -435,7 +519,7 @@ module Capybara
435
519
  # @overload within_window(window) { do_something }
436
520
  # @param window [Capybara::Window] instance of `Capybara::Window` class
437
521
  # that will be switched to
438
- # @raise [driver#no_such_window_error] if unexistent (e.g. closed) window was passed
522
+ # @raise [driver#no_such_window_error] if nonexistent (e.g. closed) window was passed
439
523
  # @overload within_window(proc_or_lambda) { do_something }
440
524
  # @param lambda [Proc] lambda. First window for which lambda
441
525
  # returns a value other than false or nil will be switched to.
@@ -446,30 +530,35 @@ module Capybara
446
530
  # @deprecated Pass window or lambda instead
447
531
  # @param [String] handle, name, url or title of the window
448
532
  #
449
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
450
- # `within_frame` or `within_window` methods
533
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within_frame` method
451
534
  # @return value returned by the block
452
535
  #
453
536
  def within_window(window_or_handle)
454
537
  if window_or_handle.instance_of?(Capybara::Window)
455
538
  original = current_window
456
- switch_to_window(window_or_handle) unless original == window_or_handle
457
539
  scopes << nil
458
540
  begin
459
- yield
541
+ _switch_to_window(window_or_handle) unless original == window_or_handle
542
+ begin
543
+ yield
544
+ ensure
545
+ _switch_to_window(original) unless original == window_or_handle
546
+ end
460
547
  ensure
461
- @scopes.pop
462
- switch_to_window(original) unless original == window_or_handle
548
+ scopes.pop
463
549
  end
464
550
  elsif window_or_handle.is_a?(Proc)
465
551
  original = current_window
466
- switch_to_window { window_or_handle.call }
467
552
  scopes << nil
468
553
  begin
469
- yield
554
+ _switch_to_window { window_or_handle.call }
555
+ begin
556
+ yield
557
+ ensure
558
+ _switch_to_window(original)
559
+ end
470
560
  ensure
471
- @scopes.pop
472
- switch_to_window(original)
561
+ scopes.pop
473
562
  end
474
563
  else
475
564
  offending_line = caller.first
@@ -480,7 +569,7 @@ module Capybara
480
569
  scopes << nil
481
570
  driver.within_window(window_or_handle) { yield }
482
571
  ensure
483
- @scopes.pop
572
+ scopes.pop
484
573
  end
485
574
  end
486
575
  end
@@ -501,7 +590,7 @@ module Capybara
501
590
  old_handles = driver.window_handles
502
591
  block.call
503
592
 
504
- wait_time = Capybara::Query.new(options).wait
593
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
505
594
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
506
595
  opened_handles = (driver.window_handles - old_handles)
507
596
  if opened_handles.size != 1
@@ -519,10 +608,16 @@ module Capybara
519
608
  # +evaluate_script+ whenever possible.
520
609
  #
521
610
  # @param [String] script A string of JavaScript to execute
611
+ # @param args Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
522
612
  #
523
- def execute_script(script)
613
+ def execute_script(script, *args)
524
614
  @touched = true
525
- driver.execute_script(script)
615
+ if args.empty?
616
+ driver.execute_script(script)
617
+ else
618
+ raise Capybara::NotSupportedByDriverError, "The current driver does not support execute_script arguments" if driver.method(:execute_script).arity == 1
619
+ driver.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg} )
620
+ end
526
621
  end
527
622
 
528
623
  ##
@@ -534,9 +629,33 @@ module Capybara
534
629
  # @param [String] script A string of JavaScript to evaluate
535
630
  # @return [Object] The result of the evaluated JavaScript (may be driver specific)
536
631
  #
537
- def evaluate_script(script)
632
+ def evaluate_script(script, *args)
538
633
  @touched = true
539
- driver.evaluate_script(script)
634
+ result = if args.empty?
635
+ driver.evaluate_script(script)
636
+ else
637
+ raise Capybara::NotSupportedByDriverError, "The current driver does not support evaluate_script arguments" if driver.method(:evaluate_script).arity == 1
638
+ driver.evaluate_script(script, *args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg} )
639
+ end
640
+ element_script_result(result)
641
+ end
642
+
643
+ ##
644
+ #
645
+ # Evaluate the given JavaScript and obtain the result from a callback function which will be passed as the last argument to the script.
646
+ #
647
+ # @param [String] script A string of JavaScript to evaluate
648
+ # @return [Object] The result of the evaluated JavaScript (may be driver specific)
649
+ #
650
+ def evaluate_async_script(script, *args)
651
+ @touched = true
652
+ result = if args.empty?
653
+ driver.evaluate_async_script(script)
654
+ else
655
+ raise Capybara::NotSupportedByDriverError, "The current driver does not support evaluate_async_script arguments" if driver.method(:evaluate_async_script).arity == 1
656
+ driver.evaluate_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg} )
657
+ end
658
+ element_script_result(result)
540
659
  end
541
660
 
542
661
  ##
@@ -544,21 +663,24 @@ module Capybara
544
663
  # Execute the block, accepting a alert.
545
664
  #
546
665
  # @!macro modal_params
666
+ # Expects a block whose actions will trigger the display modal to appear
667
+ # @example
668
+ # $0 do
669
+ # click_link('link that triggers appearance of system modal')
670
+ # end
547
671
  # @overload $0(text, options = {}, &blk)
548
672
  # @param text [String, Regexp] Text or regex to match against the text in the modal. If not provided any modal is matched
673
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time to wait for the modal to appear after executing the block.
674
+ # @yield Block whose actions will trigger the system modal
549
675
  # @overload $0(options = {}, &blk)
550
- # @option options [Numeric] :wait How long to wait for the modal to appear after executing the block.
676
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time to wait for the modal to appear after executing the block.
677
+ # @yield Block whose actions will trigger the system modal
551
678
  # @return [String] the message shown in the modal
552
679
  # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
553
680
  #
681
+ #
554
682
  def accept_alert(text_or_options=nil, options={}, &blk)
555
- if text_or_options.is_a? Hash
556
- options=text_or_options
557
- else
558
- options[:text]=text_or_options
559
- end
560
-
561
- driver.accept_modal(:alert, options, &blk)
683
+ accept_modal(:alert, text_or_options, options, &blk)
562
684
  end
563
685
 
564
686
  ##
@@ -568,13 +690,7 @@ module Capybara
568
690
  # @macro modal_params
569
691
  #
570
692
  def accept_confirm(text_or_options=nil, options={}, &blk)
571
- if text_or_options.is_a? Hash
572
- options=text_or_options
573
- else
574
- options[:text]=text_or_options
575
- end
576
-
577
- driver.accept_modal(:confirm, options, &blk)
693
+ accept_modal(:confirm, text_or_options, options, &blk)
578
694
  end
579
695
 
580
696
  ##
@@ -584,13 +700,7 @@ module Capybara
584
700
  # @macro modal_params
585
701
  #
586
702
  def dismiss_confirm(text_or_options=nil, options={}, &blk)
587
- if text_or_options.is_a? Hash
588
- options=text_or_options
589
- else
590
- options[:text]=text_or_options
591
- end
592
-
593
- driver.dismiss_modal(:confirm, options, &blk)
703
+ dismiss_modal(:confirm, text_or_options, options, &blk)
594
704
  end
595
705
 
596
706
  ##
@@ -601,13 +711,7 @@ module Capybara
601
711
  # @option options [String] :with Response to provide to the prompt
602
712
  #
603
713
  def accept_prompt(text_or_options=nil, options={}, &blk)
604
- if text_or_options.is_a? Hash
605
- options=text_or_options
606
- else
607
- options[:text]=text_or_options
608
- end
609
-
610
- driver.accept_modal(:prompt, options, &blk)
714
+ accept_modal(:prompt, text_or_options, options, &blk)
611
715
  end
612
716
 
613
717
  ##
@@ -617,13 +721,7 @@ module Capybara
617
721
  # @macro modal_params
618
722
  #
619
723
  def dismiss_prompt(text_or_options=nil, options={}, &blk)
620
- if text_or_options.is_a? Hash
621
- options=text_or_options
622
- else
623
- options[:text]=text_or_options
624
- end
625
-
626
- driver.dismiss_modal(:prompt, options, &blk)
724
+ dismiss_modal(:prompt, text_or_options, options, &blk)
627
725
  end
628
726
 
629
727
  ##
@@ -631,15 +729,18 @@ module Capybara
631
729
  # Save a snapshot of the page. If `Capybara.asset_host` is set it will inject `base` tag
632
730
  # pointing to `asset_host`.
633
731
  #
634
- # If invoked without arguments it will save file to `Capybara.save_and_open_page_path`
635
- # and file will be given randomly generated filename.
732
+ # If invoked without arguments it will save file to `Capybara.save_path`
733
+ # and file will be given randomly generated filename. If invoked with a relative path
734
+ # the path will be relative to `Capybara.save_path`, which is different from
735
+ # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
736
+ # relative to Dir.pwd
636
737
  #
637
738
  # @param [String] path the path to where it should be saved
638
739
  # @return [String] the path to which the file was saved
639
740
  #
640
741
  def save_page(path = nil)
641
742
  path = prepare_path(path, 'html')
642
- File.write(path, Capybara::Helpers.inject_asset_host(body), mode: 'wb')
743
+ File.write(path, Capybara::Helpers.inject_asset_host(body, config.asset_host), mode: 'wb')
643
744
  path
644
745
  end
645
746
 
@@ -647,8 +748,11 @@ module Capybara
647
748
  #
648
749
  # Save a snapshot of the page and open it in a browser for inspection.
649
750
  #
650
- # If invoked without arguments it will save file to `Capybara.save_and_open_page_path`
651
- # and file will be given randomly generated filename.
751
+ # If invoked without arguments it will save file to `Capybara.save_path`
752
+ # and file will be given randomly generated filename. If invoked with a relative path
753
+ # the path will be relative to `Capybara.save_path`, which is different from
754
+ # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
755
+ # relative to Dir.pwd
652
756
  #
653
757
  # @param [String] path the path to where it should be saved
654
758
  #
@@ -661,8 +765,11 @@ module Capybara
661
765
  #
662
766
  # Save a screenshot of page.
663
767
  #
664
- # If invoked without `path` argument it will save file to `Capybara.save_and_open_page_path`
665
- # and file will be given randomly generated filename.
768
+ # If invoked without arguments it will save file to `Capybara.save_path`
769
+ # and file will be given randomly generated filename. If invoked with a relative path
770
+ # the path will be relative to `Capybara.save_path`, which is different from
771
+ # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
772
+ # relative to Dir.pwd
666
773
  #
667
774
  # @param [String] path the path to where it should be saved
668
775
  # @param [Hash] options a customizable set of options
@@ -677,8 +784,11 @@ module Capybara
677
784
  #
678
785
  # Save a screenshot of the page and open it for inspection.
679
786
  #
680
- # If invoked without `path` argument it will save file to `Capybara.save_and_open_page_path`
681
- # and file will be given randomly generated filename.
787
+ # If invoked without arguments it will save file to `Capybara.save_path`
788
+ # and file will be given randomly generated filename. If invoked with a relative path
789
+ # the path will be relative to `Capybara.save_path`, which is different from
790
+ # the previous behavior with `Capybara.save_and_open_page_path` where the relative path was
791
+ # relative to Dir.pwd
682
792
  #
683
793
  # @param [String] path the path to where it should be saved
684
794
  # @param [Hash] options a customizable set of options
@@ -710,11 +820,70 @@ module Capybara
710
820
  end
711
821
 
712
822
  def current_scope
713
- scopes.last || document
823
+ scope = scopes.last
824
+ scope = document if [nil, :frame].include? scope
825
+ scope
714
826
  end
715
827
 
828
+ ##
829
+ #
830
+ # Yield a block using a specific wait time
831
+ #
832
+ def using_wait_time(seconds)
833
+ if Capybara.threadsafe
834
+ begin
835
+ previous_wait_time = config.default_max_wait_time
836
+ config.default_max_wait_time = seconds
837
+ yield
838
+ ensure
839
+ config.default_max_wait_time = previous_wait_time
840
+ end
841
+ else
842
+ Capybara.using_wait_time(seconds) { yield }
843
+ end
844
+ end
845
+
846
+ ##
847
+ #
848
+ # Accepts a block to set the configuration options if Capybara.threadsafe == true. Note that some options only have an effect
849
+ # if set at initialization time, so look at the configuration block that can be passed to the initializer too
850
+ #
851
+ def configure
852
+ raise "Session configuration is only supported when Capybara.threadsafe == true" unless Capybara.threadsafe
853
+ yield config
854
+ end
855
+
856
+ def self.instance_created?
857
+ @@instance_created
858
+ end
859
+
860
+ def config
861
+ @config ||= if Capybara.threadsafe
862
+ Capybara.session_options.dup
863
+ else
864
+ Capybara::ReadOnlySessionConfig.new(Capybara.session_options)
865
+ end
866
+ end
716
867
  private
717
868
 
869
+ @@instance_created = false
870
+
871
+ def accept_modal(type, text_or_options, options, &blk)
872
+ driver.accept_modal(type, modal_options(text_or_options, options), &blk)
873
+ end
874
+
875
+ def dismiss_modal(type, text_or_options, options, &blk)
876
+ driver.dismiss_modal(type, modal_options(text_or_options, options), &blk)
877
+ end
878
+
879
+ def modal_options(text_or_options, options)
880
+ text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
881
+ options[:text] ||= text_or_options unless text_or_options.nil?
882
+ options[:wait] ||= config.default_max_wait_time
883
+ options
884
+ end
885
+
886
+
718
887
  def open_file(path)
719
888
  begin
720
889
  require "launchy"
@@ -726,19 +895,85 @@ module Capybara
726
895
  end
727
896
 
728
897
  def prepare_path(path, extension)
729
- path = default_path(extension) if path.nil?
898
+ if config.save_path || config.save_and_open_page_path.nil?
899
+ path = File.expand_path(path || default_fn(extension), config.save_path)
900
+ else
901
+ path = File.expand_path(default_fn(extension), config.save_and_open_page_path) if path.nil?
902
+ end
730
903
  FileUtils.mkdir_p(File.dirname(path))
731
904
  path
732
905
  end
733
906
 
734
- def default_path(extension)
907
+ def default_fn(extension)
735
908
  timestamp = Time.new.strftime("%Y%m%d%H%M%S")
736
- path = "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
737
- File.expand_path(path, Capybara.save_and_open_page_path)
909
+ "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
738
910
  end
739
911
 
740
912
  def scopes
741
913
  @scopes ||= [nil]
742
914
  end
915
+
916
+ def element_script_result(arg)
917
+ case arg
918
+ when Array
919
+ arg.map { |e| element_script_result(e) }
920
+ when Hash
921
+ arg.each { |k, v| arg[k] = element_script_result(v) }
922
+ when Capybara::Driver::Node
923
+ Capybara::Node::Element.new(self, arg, nil, nil)
924
+ else
925
+ arg
926
+ end
927
+ end
928
+
929
+ def _find_frame(*args)
930
+ within(document) do # Previous 2.x versions ignored current scope when finding frames - consider changing in 3.0
931
+ case args[0]
932
+ when Capybara::Node::Element
933
+ args[0]
934
+ when String, Hash
935
+ find(:frame, *args)
936
+ when Symbol
937
+ find(*args)
938
+ when Integer
939
+ idx = args[0]
940
+ all(:frame, minimum: idx+1)[idx]
941
+ else
942
+ raise TypeError
943
+ end
944
+ end
945
+ end
946
+
947
+ def _switch_to_window(window = nil, options= {})
948
+ options, window = window, nil if window.is_a? Hash
949
+
950
+ raise Capybara::ScopeError, "Window cannot be switched inside a `within_frame` block" if scopes.include?(:frame)
951
+ raise Capybara::ScopeError, "Window cannot be switch inside a `within` block" unless scopes.last.nil?
952
+
953
+ if window
954
+ driver.switch_to_window(window.handle)
955
+ window
956
+ else
957
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
958
+ document.synchronize(wait_time, errors: [Capybara::WindowError]) do
959
+ original_window_handle = driver.current_window_handle
960
+ begin
961
+ driver.window_handles.each do |handle|
962
+ driver.switch_to_window handle
963
+ if yield
964
+ return Window.new(self, handle)
965
+ end
966
+ end
967
+ rescue => e
968
+ driver.switch_to_window(original_window_handle)
969
+ raise e
970
+ else
971
+ driver.switch_to_window(original_window_handle)
972
+ raise Capybara::WindowError, "Could not find a window matching block/lambda"
973
+ end
974
+ end
975
+ end
976
+ end
977
+
743
978
  end
744
979
  end