capybara 2.5.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
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