capybara 2.13.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +218 -18
  3. data/README.md +54 -23
  4. data/lib/capybara/config.rb +132 -0
  5. data/lib/capybara/cucumber.rb +1 -0
  6. data/lib/capybara/driver/base.rb +14 -0
  7. data/lib/capybara/dsl.rb +1 -3
  8. data/lib/capybara/helpers.rb +3 -3
  9. data/lib/capybara/minitest/spec.rb +14 -37
  10. data/lib/capybara/minitest.rb +95 -114
  11. data/lib/capybara/node/actions.rb +10 -10
  12. data/lib/capybara/node/base.rb +7 -2
  13. data/lib/capybara/node/element.rb +9 -3
  14. data/lib/capybara/node/finders.rb +92 -18
  15. data/lib/capybara/node/matchers.rb +21 -9
  16. data/lib/capybara/node/simple.rb +5 -0
  17. data/lib/capybara/queries/ancestor_query.rb +25 -0
  18. data/lib/capybara/queries/base_query.rb +12 -3
  19. data/lib/capybara/queries/current_path_query.rb +13 -9
  20. data/lib/capybara/queries/selector_query.rb +62 -23
  21. data/lib/capybara/queries/sibling_query.rb +25 -0
  22. data/lib/capybara/queries/text_query.rb +10 -5
  23. data/lib/capybara/queries/title_query.rb +1 -0
  24. data/lib/capybara/rack_test/browser.rb +13 -5
  25. data/lib/capybara/rack_test/driver.rb +6 -1
  26. data/lib/capybara/rack_test/form.rb +4 -3
  27. data/lib/capybara/rack_test/node.rb +1 -1
  28. data/lib/capybara/rspec/compound.rb +95 -0
  29. data/lib/capybara/rspec/matcher_proxies.rb +45 -0
  30. data/lib/capybara/rspec/matchers.rb +108 -7
  31. data/lib/capybara/rspec.rb +3 -1
  32. data/lib/capybara/selector/filter.rb +13 -41
  33. data/lib/capybara/selector/filter_set.rb +30 -4
  34. data/lib/capybara/selector/filters/base.rb +33 -0
  35. data/lib/capybara/selector/filters/expression_filter.rb +40 -0
  36. data/lib/capybara/selector/filters/node_filter.rb +27 -0
  37. data/lib/capybara/selector/selector.rb +36 -15
  38. data/lib/capybara/selector.rb +63 -42
  39. data/lib/capybara/selenium/driver.rb +177 -33
  40. data/lib/capybara/selenium/node.rb +106 -55
  41. data/lib/capybara/server.rb +6 -5
  42. data/lib/capybara/session/config.rb +114 -0
  43. data/lib/capybara/session/matchers.rb +15 -4
  44. data/lib/capybara/session.rb +178 -65
  45. data/lib/capybara/spec/fixtures/no_extension +1 -0
  46. data/lib/capybara/spec/public/test.js +18 -3
  47. data/lib/capybara/spec/session/accept_alert_spec.rb +9 -1
  48. data/lib/capybara/spec/session/accept_prompt_spec.rb +29 -1
  49. data/lib/capybara/spec/session/all_spec.rb +13 -1
  50. data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
  51. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +24 -8
  52. data/lib/capybara/spec/session/assert_selector.rb +1 -1
  53. data/lib/capybara/spec/session/assert_text.rb +8 -0
  54. data/lib/capybara/spec/session/assert_title.rb +22 -9
  55. data/lib/capybara/spec/session/attach_file_spec.rb +8 -1
  56. data/lib/capybara/spec/session/check_spec.rb +4 -4
  57. data/lib/capybara/spec/session/choose_spec.rb +2 -2
  58. data/lib/capybara/spec/session/click_button_spec.rb +1 -1
  59. data/lib/capybara/spec/session/click_link_or_button_spec.rb +3 -3
  60. data/lib/capybara/spec/session/click_link_spec.rb +1 -1
  61. data/lib/capybara/spec/session/current_url_spec.rb +3 -3
  62. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  63. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -1
  64. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +22 -0
  65. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -1
  66. data/lib/capybara/spec/session/fill_in_spec.rb +8 -2
  67. data/lib/capybara/spec/session/find_field_spec.rb +1 -0
  68. data/lib/capybara/spec/session/find_spec.rb +8 -6
  69. data/lib/capybara/spec/session/first_spec.rb +10 -5
  70. data/lib/capybara/spec/session/has_all_selectors_spec.rb +69 -0
  71. data/lib/capybara/spec/session/has_css_spec.rb +11 -0
  72. data/lib/capybara/spec/session/has_current_path_spec.rb +52 -7
  73. data/lib/capybara/spec/session/has_link_spec.rb +4 -4
  74. data/lib/capybara/spec/session/has_none_selectors_spec.rb +76 -0
  75. data/lib/capybara/spec/session/has_select_spec.rb +64 -6
  76. data/lib/capybara/spec/session/has_selector_spec.rb +1 -3
  77. data/lib/capybara/spec/session/has_text_spec.rb +5 -3
  78. data/lib/capybara/spec/session/has_title_spec.rb +4 -2
  79. data/lib/capybara/spec/session/has_xpath_spec.rb +5 -3
  80. data/lib/capybara/spec/session/node_spec.rb +50 -26
  81. data/lib/capybara/spec/session/refresh_spec.rb +28 -0
  82. data/lib/capybara/spec/session/reset_session_spec.rb +3 -3
  83. data/lib/capybara/spec/session/select_spec.rb +3 -2
  84. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  85. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  86. data/lib/capybara/spec/session/unselect_spec.rb +2 -2
  87. data/lib/capybara/spec/session/visit_spec.rb +56 -1
  88. data/lib/capybara/spec/session/window/become_closed_spec.rb +11 -11
  89. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +11 -9
  90. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -4
  91. data/lib/capybara/spec/session/window/within_window_spec.rb +27 -2
  92. data/lib/capybara/spec/spec_helper.rb +28 -4
  93. data/lib/capybara/spec/test_app.rb +3 -1
  94. data/lib/capybara/spec/views/form.erb +27 -1
  95. data/lib/capybara/spec/views/initial_alert.erb +10 -0
  96. data/lib/capybara/spec/views/with_fixed_header_footer.erb +17 -0
  97. data/lib/capybara/spec/views/with_hover.erb +5 -0
  98. data/lib/capybara/spec/views/with_html.erb +33 -2
  99. data/lib/capybara/spec/views/with_js.erb +12 -0
  100. data/lib/capybara/spec/views/with_windows.erb +4 -0
  101. data/lib/capybara/version.rb +1 -1
  102. data/lib/capybara/window.rb +1 -1
  103. data/lib/capybara.rb +102 -124
  104. data/spec/capybara_spec.rb +43 -21
  105. data/spec/dsl_spec.rb +1 -0
  106. data/spec/filter_set_spec.rb +28 -0
  107. data/spec/minitest_spec.rb +9 -1
  108. data/spec/minitest_spec_spec.rb +19 -5
  109. data/spec/per_session_config_spec.rb +67 -0
  110. data/spec/result_spec.rb +20 -0
  111. data/spec/rspec/shared_spec_matchers.rb +148 -44
  112. data/spec/rspec/views_spec.rb +4 -0
  113. data/spec/rspec_matchers_spec.rb +46 -0
  114. data/spec/rspec_spec.rb +77 -0
  115. data/spec/selector_spec.rb +2 -1
  116. data/spec/selenium_spec_chrome.rb +25 -17
  117. data/spec/selenium_spec_firefox.rb +2 -1
  118. data/spec/selenium_spec_marionette.rb +18 -5
  119. data/spec/session_spec.rb +44 -0
  120. data/spec/shared_selenium_session.rb +72 -8
  121. data/spec/spec_helper.rb +4 -0
  122. metadata +55 -8
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ require 'delegate'
3
+
4
+ module Capybara
5
+ class SessionConfig
6
+ OPTIONS = [:always_include_port, :run_server, :default_selector, :default_max_wait_time, :ignore_hidden_elements,
7
+ :automatic_reload, :match, :exact, :exact_text, :raise_server_errors, :visible_text_only, :wait_on_first_by_default,
8
+ :automatic_label_click, :enable_aria_label, :save_path, :exact_options, :asset_host, :default_host, :app_host,
9
+ :save_and_open_page_path, :server_host, :server_port, :server_errors]
10
+
11
+ attr_accessor(*OPTIONS)
12
+
13
+ ##
14
+ #@!method always_include_port
15
+ # See {Capybara.configure}
16
+ #@!method run_server
17
+ # See {Capybara.configure}
18
+ #@!method default_selector
19
+ # See {Capybara.configure}
20
+ #@!method default_max_wait_time
21
+ # See {Capybara.configure}
22
+ #@!method ignore_hidden_elements
23
+ # See {Capybara.configure}
24
+ #@!method automatic_reload
25
+ # See {Capybara.configure}
26
+ #@!method match
27
+ # See {Capybara.configure}
28
+ #@!method exact
29
+ # See {Capybara.configure}
30
+ #@!method raise_server_errors
31
+ # See {Capybara.configure}
32
+ #@!method visible_text_only
33
+ # See {Capybara.configure}
34
+ #@!method wait_on_first_by_default
35
+ # See {Capybara.configure}
36
+ #@!method automatic_label_click
37
+ # See {Capybara.configure}
38
+ #@!method enable_aria_label
39
+ # See {Capybara.configure}
40
+ #@!method save_path
41
+ # See {Capybara.configure}
42
+ #@deprecated
43
+ #@!method exact_options
44
+ # See {Capybara.configure}
45
+ #@!method asset_host
46
+ # See {Capybara.configure}
47
+ #@!method default_host
48
+ # See {Capybara.configure}
49
+ #@!method app_host
50
+ # See {Capybara.configure}
51
+ #@!method save_and_open_page_path
52
+ # See {Capybara.configure}
53
+ #@!method server_host
54
+ # See {Capybara.configure}
55
+ #@!method server_port
56
+ # See {Capybara.configure}
57
+ #@!method server_errors
58
+ # See {Capybara.configure}
59
+
60
+ remove_method :server_host
61
+
62
+ ##
63
+ #
64
+ # @return [String] The IP address bound by default server
65
+ #
66
+ def server_host
67
+ @server_host || '127.0.0.1'
68
+ end
69
+
70
+ remove_method :server_errors=
71
+ def server_errors=(errors)
72
+ (@server_errors ||= []).replace(errors.dup)
73
+ end
74
+
75
+ remove_method :app_host=
76
+ def app_host=(url)
77
+ raise ArgumentError.new("Capybara.app_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
78
+ @app_host = url
79
+ end
80
+
81
+ remove_method :default_host=
82
+ def default_host=(url)
83
+ raise ArgumentError.new("Capybara.default_host should be set to a url (http://www.example.com)") unless url.nil? || (url =~ URI::Parser.new.make_regexp)
84
+ @default_host = url
85
+ end
86
+
87
+ remove_method :save_and_open_page_path=
88
+ def save_and_open_page_path=(path)
89
+ warn "DEPRECATED: #save_and_open_page_path is deprecated, please use #save_path instead. \n"\
90
+ "Note: Behavior is slightly different with relative paths - see documentation" unless path.nil?
91
+ @save_and_open_page_path = path
92
+ end
93
+
94
+ remove_method :exact_options=
95
+ def exact_options=(opt)
96
+ @exact_options = opt
97
+ warn "DEPRECATED: #exact_options is deprecated, please scope your findes/actions and use the `:exact` "\
98
+ "option if similar functionality is needed."
99
+ end
100
+
101
+ def initialize_copy(other)
102
+ super
103
+ @server_errors = @server_errors.dup
104
+ end
105
+ end
106
+
107
+ class ReadOnlySessionConfig < SimpleDelegator
108
+ SessionConfig::OPTIONS.each do |m|
109
+ define_method "#{m}=" do |val|
110
+ raise "Per session settings are only supported when Capybara.threadsafe == true"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -3,16 +3,18 @@ module Capybara
3
3
  module SessionMatchers
4
4
  ##
5
5
  # Asserts that the page has the given path.
6
- # By default this will compare against the path+query portion of the full url
6
+ # By default, if passed a full url this will compare against the full url,
7
+ # if passed a path only the path+query portion will be compared, if passed a regexp
8
+ # the comparison will depend on the :url option
7
9
  #
8
10
  # @!macro current_path_query_params
9
11
  # @overload $0(string, options = {})
10
12
  # @param string [String] The string that the current 'path' should equal
11
13
  # @overload $0(regexp, options = {})
12
14
  # @param regexp [Regexp] The regexp that the current 'path' should match to
13
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current path to eq/match given string/regexp argument
14
- # @option options [Boolean] :url (false) Whether the compare should be done against the full url
15
- # @option options [Boolean] :only_path (false) Whether the compare should be done against just the path protion of the url
15
+ # @option options [Boolean] :url (true if `string` ia a full url, otherwise false) Whether the compare should be done against the full current url or just the path
16
+ # @option options [Boolean] :ignore_query (false) Whether the query portion of the current url/path should be ignored
17
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for the current url/path to eq/match given string/regexp argument
16
18
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
17
19
  # @return [true]
18
20
  #
@@ -22,6 +24,9 @@ module Capybara
22
24
 
23
25
  ##
24
26
  # Asserts that the page doesn't have the given path.
27
+ # By default, if passed a full url this will compare against the full url,
28
+ # if passed a path only the path+query portion will be compared, if passed a regexp
29
+ # the comparison will depend on the :url option
25
30
  #
26
31
  # @macro current_path_query_params
27
32
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
@@ -33,6 +38,9 @@ module Capybara
33
38
 
34
39
  ##
35
40
  # Checks if the page has the given path.
41
+ # By default, if passed a full url this will compare against the full url,
42
+ # if passed a path only the path+query portion will be compared, if passed a regexp
43
+ # the comparison will depend on the :url option
36
44
  #
37
45
  # @macro current_path_query_params
38
46
  # @return [Boolean]
@@ -45,6 +53,9 @@ module Capybara
45
53
 
46
54
  ##
47
55
  # Checks if the page doesn't have the given path.
56
+ # By default, if passed a full url this will compare against the full url,
57
+ # if passed a path only the path+query portion will be compared, if passed a regexp
58
+ # the comparison will depend on the :url option
48
59
  #
49
60
  # @macro current_path_query_params
50
61
  # @return [Boolean]
@@ -17,6 +17,14 @@ module Capybara
17
17
  # session = Capybara::Session.new(:culerity)
18
18
  # session.visit('http://www.google.com')
19
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
+ #
20
28
  # Session provides a number of methods for controlling the navigation of the page, such as +visit+,
21
29
  # +current_path, and so on. It also delegate a number of methods to a Capybara::Document, representing
22
30
  # the current HTML document. This allows interaction:
@@ -50,7 +58,7 @@ module Capybara
50
58
  ]
51
59
  SESSION_METHODS = [
52
60
  :body, :html, :source, :current_url, :current_host, :current_path,
53
- :execute_script, :evaluate_script, :visit, :go_back, :go_forward,
61
+ :execute_script, :evaluate_script, :visit, :refresh, :go_back, :go_forward,
54
62
  :within, :within_element, :within_fieldset, :within_table, :within_frame, :switch_to_frame,
55
63
  :current_window, :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
56
64
  :save_page, :save_and_open_page, :save_screenshot,
@@ -69,10 +77,15 @@ module Capybara
69
77
 
70
78
  def initialize(mode, app=nil)
71
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
72
81
  @mode = mode
73
82
  @app = app
74
- if Capybara.run_server and @app and driver.needs_server?
75
- @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
76
89
  else
77
90
  @server = nil
78
91
  end
@@ -85,7 +98,9 @@ module Capybara
85
98
  other_drivers = Capybara.drivers.keys.map { |key| key.inspect }
86
99
  raise Capybara::DriverNotFoundError, "no driver called #{mode.inspect} was found, available drivers: #{other_drivers.join(', ')}"
87
100
  end
88
- Capybara.drivers[mode].call(app)
101
+ driver = Capybara.drivers[mode].call(app)
102
+ driver.session = self if driver.respond_to?(:session=)
103
+ driver
89
104
  end
90
105
  end
91
106
 
@@ -126,7 +141,7 @@ module Capybara
126
141
  if @server and @server.error
127
142
  # Force an explanation for the error being raised as the exception cause
128
143
  begin
129
- if Capybara.raise_server_errors
144
+ if config.raise_server_errors
130
145
  raise CapybaraError, "Your application server raised an error - It has been raised in your test code because Capybara.raise_server_errors == true"
131
146
  end
132
147
  rescue CapybaraError
@@ -232,28 +247,42 @@ module Capybara
232
247
  raise_server_error!
233
248
  @touched = true
234
249
 
235
- visit_uri = URI.parse(visit_uri.to_s)
250
+ visit_uri = ::Addressable::URI.parse(visit_uri.to_s)
236
251
 
237
252
  uri_base = if @server
238
- visit_uri.port = @server.port if Capybara.always_include_port && (visit_uri.port == visit_uri.default_port)
239
- URI.parse(Capybara.app_host || "http://#{@server.host}:#{@server.port}")
253
+ ::Addressable::URI.parse(config.app_host || "http://#{@server.host}:#{@server.port}")
240
254
  else
241
- Capybara.app_host && URI.parse(Capybara.app_host)
255
+ config.app_host && ::Addressable::URI.parse(config.app_host)
242
256
  end
243
257
 
244
- # TODO - this is only for compatability with previous 2.x behavior that concatenated
245
- # Capybara.app_host and a "relative" path - Consider removing in 3.0
246
- # @abotalov brought up a good point about this behavior potentially being useful to people
247
- # deploying to a subdirectory and/or single page apps where only the url fragment changes
248
- if visit_uri.scheme.nil? && uri_base
249
- visit_uri.path = uri_base.path + visit_uri.path
250
- end
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
251
261
 
252
- visit_uri = uri_base.merge(visit_uri) unless uri_base.nil?
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
267
+
268
+ visit_uri = uri_base.merge(visit_uri_parts)
269
+ else
270
+ visit_uri.port ||= @server.port if @server && config.always_include_port
271
+ end
272
+ end
253
273
 
254
274
  driver.visit(visit_uri.to_s)
255
275
  end
256
276
 
277
+ ##
278
+ #
279
+ # Refresh the page
280
+ #
281
+ def refresh
282
+ raise_server_error!
283
+ driver.refresh
284
+ end
285
+
257
286
  ##
258
287
  #
259
288
  # Move back a single entry in the browser's history.
@@ -457,14 +486,14 @@ module Capybara
457
486
  # @raise [Capybara::WindowError] if no window matches given block
458
487
  # @overload switch_to_window(window)
459
488
  # @param window [Capybara::Window] window that should be switched to
460
- # @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
461
490
  #
462
491
  # @return [Capybara::Window] window that has been switched to
463
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
464
- # `within_frame` or `within_window` methods
492
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within` or
493
+ # `within_frame` methods
465
494
  # @raise [ArgumentError] if both or neither arguments were provided
466
495
  #
467
- def switch_to_window(window = nil, options= {})
496
+ def switch_to_window(window = nil, options= {}, &window_locator)
468
497
  options, window = window, nil if window.is_a? Hash
469
498
 
470
499
  block_given = block_given?
@@ -472,34 +501,12 @@ module Capybara
472
501
  raise ArgumentError, "`switch_to_window` can take either a block or a window, not both"
473
502
  elsif !window && !block_given
474
503
  raise ArgumentError, "`switch_to_window`: either window or block should be provided"
475
- elsif scopes.size > 1
504
+ elsif !scopes.last.nil?
476
505
  raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\
477
- "`within`'s, `within_frame`'s' or `within_window`'s' block."
506
+ "`within` or `within_frame` blocks."
478
507
  end
479
508
 
480
- if window
481
- driver.switch_to_window(window.handle)
482
- window
483
- else
484
- wait_time = Capybara::Queries::BaseQuery.wait(options)
485
- document.synchronize(wait_time, errors: [Capybara::WindowError]) do
486
- original_window_handle = driver.current_window_handle
487
- begin
488
- driver.window_handles.each do |handle|
489
- driver.switch_to_window handle
490
- if yield
491
- return Window.new(self, handle)
492
- end
493
- end
494
- rescue => e
495
- driver.switch_to_window(original_window_handle)
496
- raise e
497
- else
498
- driver.switch_to_window(original_window_handle)
499
- raise Capybara::WindowError, "Could not find a window matching block/lambda"
500
- end
501
- end
502
- end
509
+ _switch_to_window(window, options, &window_locator)
503
510
  end
504
511
 
505
512
  ##
@@ -512,7 +519,7 @@ module Capybara
512
519
  # @overload within_window(window) { do_something }
513
520
  # @param window [Capybara::Window] instance of `Capybara::Window` class
514
521
  # that will be switched to
515
- # @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
516
523
  # @overload within_window(proc_or_lambda) { do_something }
517
524
  # @param lambda [Proc] lambda. First window for which lambda
518
525
  # returns a value other than false or nil will be switched to.
@@ -523,30 +530,35 @@ module Capybara
523
530
  # @deprecated Pass window or lambda instead
524
531
  # @param [String] handle, name, url or title of the window
525
532
  #
526
- # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
527
- # `within_frame` or `within_window` methods
533
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within_frame` method
528
534
  # @return value returned by the block
529
535
  #
530
536
  def within_window(window_or_handle)
531
537
  if window_or_handle.instance_of?(Capybara::Window)
532
538
  original = current_window
533
- switch_to_window(window_or_handle) unless original == window_or_handle
534
539
  scopes << nil
535
540
  begin
536
- 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
537
547
  ensure
538
- @scopes.pop
539
- switch_to_window(original) unless original == window_or_handle
548
+ scopes.pop
540
549
  end
541
550
  elsif window_or_handle.is_a?(Proc)
542
551
  original = current_window
543
- switch_to_window { window_or_handle.call }
544
552
  scopes << nil
545
553
  begin
546
- yield
554
+ _switch_to_window { window_or_handle.call }
555
+ begin
556
+ yield
557
+ ensure
558
+ _switch_to_window(original)
559
+ end
547
560
  ensure
548
- @scopes.pop
549
- switch_to_window(original)
561
+ scopes.pop
550
562
  end
551
563
  else
552
564
  offending_line = caller.first
@@ -557,7 +569,7 @@ module Capybara
557
569
  scopes << nil
558
570
  driver.within_window(window_or_handle) { yield }
559
571
  ensure
560
- @scopes.pop
572
+ scopes.pop
561
573
  end
562
574
  end
563
575
  end
@@ -578,7 +590,7 @@ module Capybara
578
590
  old_handles = driver.window_handles
579
591
  block.call
580
592
 
581
- wait_time = Capybara::Queries::BaseQuery.wait(options)
593
+ wait_time = Capybara::Queries::BaseQuery.wait(options, config.default_max_wait_time)
582
594
  document.synchronize(wait_time, errors: [Capybara::WindowError]) do
583
595
  opened_handles = (driver.window_handles - old_handles)
584
596
  if opened_handles.size != 1
@@ -628,18 +640,45 @@ module Capybara
628
640
  element_script_result(result)
629
641
  end
630
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)
659
+ end
660
+
631
661
  ##
632
662
  #
633
663
  # Execute the block, accepting a alert.
634
664
  #
635
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
636
671
  # @overload $0(text, options = {}, &blk)
637
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
638
675
  # @overload $0(options = {}, &blk)
639
- # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time 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
640
678
  # @return [String] the message shown in the modal
641
679
  # @raise [Capybara::ModalNotFound] if modal dialog hasn't been found
642
680
  #
681
+ #
643
682
  def accept_alert(text_or_options=nil, options={}, &blk)
644
683
  accept_modal(:alert, text_or_options, options, &blk)
645
684
  end
@@ -701,7 +740,7 @@ module Capybara
701
740
  #
702
741
  def save_page(path = nil)
703
742
  path = prepare_path(path, 'html')
704
- 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')
705
744
  path
706
745
  end
707
746
 
@@ -786,7 +825,49 @@ module Capybara
786
825
  scope
787
826
  end
788
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
789
867
  private
868
+
869
+ @@instance_created = false
870
+
790
871
  def accept_modal(type, text_or_options, options, &blk)
791
872
  driver.accept_modal(type, modal_options(text_or_options, options), &blk)
792
873
  end
@@ -798,7 +879,7 @@ module Capybara
798
879
  def modal_options(text_or_options, options)
799
880
  text_or_options, options = nil, text_or_options if text_or_options.is_a?(Hash)
800
881
  options[:text] ||= text_or_options unless text_or_options.nil?
801
- options[:wait] ||= Capybara.default_max_wait_time
882
+ options[:wait] ||= config.default_max_wait_time
802
883
  options
803
884
  end
804
885
 
@@ -814,10 +895,10 @@ module Capybara
814
895
  end
815
896
 
816
897
  def prepare_path(path, extension)
817
- if Capybara.save_path || Capybara.save_and_open_page_path.nil?
818
- path = File.expand_path(path || default_fn(extension), Capybara.save_path)
898
+ if config.save_path || config.save_and_open_page_path.nil?
899
+ path = File.expand_path(path || default_fn(extension), config.save_path)
819
900
  else
820
- path = File.expand_path(default_fn(extension), Capybara.save_and_open_page_path) if path.nil?
901
+ path = File.expand_path(default_fn(extension), config.save_and_open_page_path) if path.nil?
821
902
  end
822
903
  FileUtils.mkdir_p(File.dirname(path))
823
904
  path
@@ -862,5 +943,37 @@ module Capybara
862
943
  end
863
944
  end
864
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
+
865
978
  end
866
979
  end
@@ -0,0 +1 @@
1
+ ThisFileHAsNoExtension
@@ -1,8 +1,8 @@
1
1
  var activeRequests = 0;
2
2
  $(function() {
3
3
  $('#change').text('I changed it');
4
- $('#drag').draggable();
5
- $('#drop').droppable({
4
+ $('#drag, #drag_scroll').draggable();
5
+ $('#drop, #drop_scroll').droppable({
6
6
  drop: function(event, ui) {
7
7
  ui.draggable.remove();
8
8
  $(this).html('Dropped!');
@@ -37,6 +37,9 @@ $(function() {
37
37
  $('#with_change_event').change(function() {
38
38
  $('body').append($('<p class="change_event_triggered"></p>').text(this.value));
39
39
  });
40
+ $('#with_change_event').on('input', function() {
41
+ $('body').append($('<p class="input_event_triggered"></p>').text(this.value));
42
+ });
40
43
  $('#checkbox_with_event').click(function() {
41
44
  $('body').append('<p id="checkbox_event_triggered">Checkbox event triggered</p>');
42
45
  });
@@ -58,7 +61,7 @@ $(function() {
58
61
  $('#change-title').click(function() {
59
62
  setTimeout(function() {
60
63
  $('title').text('changed title')
61
- }, 250)
64
+ }, 400)
62
65
  });
63
66
  $('#click-test').on({
64
67
  dblclick: function() {
@@ -87,6 +90,10 @@ $(function() {
87
90
  $(link).attr('opened', 'true');
88
91
  }, 3000);
89
92
  });
93
+ $('#alert-page-change').click(function() {
94
+ alert('Page is changing');
95
+ return true;
96
+ });
90
97
  $('#open-confirm').click(function() {
91
98
  if(confirm('Confirm opened')) {
92
99
  $(this).attr('confirmed', 'true');
@@ -102,6 +109,14 @@ $(function() {
102
109
  $(this).attr('response', response);
103
110
  }
104
111
  });
112
+ $('#open-prompt-with-default').click(function() {
113
+ var response = prompt('Prompt opened', 'Default value!');
114
+ if(response === null) {
115
+ $(this).attr('response', 'dismissed');
116
+ } else {
117
+ $(this).attr('response', response);
118
+ }
119
+ });
105
120
  $('#open-twice').click(function() {
106
121
  if (confirm('Are you sure?')) {
107
122
  if (!confirm('Are you really sure?')) {
@@ -47,6 +47,14 @@ Capybara::SpecHelper.spec '#accept_alert', requires: [:modals] do
47
47
  expect(message).to eq('Alert opened [*Yay?*]')
48
48
  end
49
49
 
50
+ it "should handle the alert if the page changes" do
51
+ @session.accept_alert do
52
+ @session.click_link('Alert page change')
53
+ sleep 1 # ensure page change occurs before the accept_alert block exits
54
+ end
55
+ expect(@session).to have_current_path('/with_html')
56
+ end
57
+
50
58
  context "with an asynchronous alert" do
51
59
  it "should accept the alert" do
52
60
  @session.accept_alert do
@@ -63,7 +71,7 @@ Capybara::SpecHelper.spec '#accept_alert', requires: [:modals] do
63
71
  end
64
72
 
65
73
  it "should allow to adjust the delay" do
66
- @session.accept_alert wait: 4 do
74
+ @session.accept_alert wait: 10 do
67
75
  @session.click_link('Open slow alert')
68
76
  end
69
77
  expect(@session).to have_xpath("//a[@id='open-slow-alert' and @opened='true']")