capybara 2.2.1 → 2.3.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +26 -0
  3. data/README.md +36 -14
  4. data/lib/capybara.rb +6 -3
  5. data/lib/capybara/driver/base.rb +37 -1
  6. data/lib/capybara/driver/node.rb +10 -2
  7. data/lib/capybara/helpers.rb +21 -13
  8. data/lib/capybara/node/base.rb +12 -7
  9. data/lib/capybara/node/element.rb +17 -1
  10. data/lib/capybara/node/finders.rb +22 -1
  11. data/lib/capybara/node/matchers.rb +26 -5
  12. data/lib/capybara/node/simple.rb +9 -2
  13. data/lib/capybara/rack_test/css_handlers.rb +3 -1
  14. data/lib/capybara/rack_test/form.rb +3 -2
  15. data/lib/capybara/rack_test/node.rb +3 -3
  16. data/lib/capybara/rspec.rb +1 -0
  17. data/lib/capybara/rspec/features.rb +2 -1
  18. data/lib/capybara/rspec/matchers.rb +50 -5
  19. data/lib/capybara/selenium/driver.rb +76 -12
  20. data/lib/capybara/selenium/node.rb +8 -0
  21. data/lib/capybara/server.rb +1 -1
  22. data/lib/capybara/session.rb +234 -29
  23. data/lib/capybara/spec/public/jquery.js +1 -1
  24. data/lib/capybara/spec/public/test.js +7 -0
  25. data/lib/capybara/spec/session/all_spec.rb +88 -17
  26. data/lib/capybara/spec/session/assert_selector.rb +6 -0
  27. data/lib/capybara/spec/session/attach_file_spec.rb +15 -15
  28. data/lib/capybara/spec/session/body_spec.rb +4 -4
  29. data/lib/capybara/spec/session/check_spec.rb +16 -16
  30. data/lib/capybara/spec/session/choose_spec.rb +5 -5
  31. data/lib/capybara/spec/session/click_button_spec.rb +93 -84
  32. data/lib/capybara/spec/session/click_link_or_button_spec.rb +8 -8
  33. data/lib/capybara/spec/session/click_link_spec.rb +26 -19
  34. data/lib/capybara/spec/session/current_scope_spec.rb +3 -3
  35. data/lib/capybara/spec/session/current_url_spec.rb +8 -8
  36. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -1
  37. data/lib/capybara/spec/session/execute_script_spec.rb +2 -2
  38. data/lib/capybara/spec/session/fill_in_spec.rb +22 -22
  39. data/lib/capybara/spec/session/find_button_spec.rb +4 -4
  40. data/lib/capybara/spec/session/find_by_id_spec.rb +3 -3
  41. data/lib/capybara/spec/session/find_field_spec.rb +7 -7
  42. data/lib/capybara/spec/session/find_link_spec.rb +4 -4
  43. data/lib/capybara/spec/session/find_spec.rb +46 -46
  44. data/lib/capybara/spec/session/first_spec.rb +23 -23
  45. data/lib/capybara/spec/session/go_back_spec.rb +3 -3
  46. data/lib/capybara/spec/session/go_forward_spec.rb +4 -4
  47. data/lib/capybara/spec/session/has_button_spec.rb +13 -13
  48. data/lib/capybara/spec/session/has_css_spec.rb +87 -87
  49. data/lib/capybara/spec/session/has_field_spec.rb +87 -87
  50. data/lib/capybara/spec/session/has_link_spec.rb +11 -11
  51. data/lib/capybara/spec/session/has_select_spec.rb +58 -58
  52. data/lib/capybara/spec/session/has_selector_spec.rb +48 -48
  53. data/lib/capybara/spec/session/has_table_spec.rb +7 -7
  54. data/lib/capybara/spec/session/has_text_spec.rb +73 -73
  55. data/lib/capybara/spec/session/has_title_spec.rb +10 -10
  56. data/lib/capybara/spec/session/has_xpath_spec.rb +44 -44
  57. data/lib/capybara/spec/session/headers.rb +1 -1
  58. data/lib/capybara/spec/session/html_spec.rb +9 -9
  59. data/lib/capybara/spec/session/node_spec.rb +81 -65
  60. data/lib/capybara/spec/session/reset_session_spec.rb +15 -15
  61. data/lib/capybara/spec/session/response_code.rb +1 -1
  62. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +46 -0
  63. data/lib/capybara/spec/session/save_page_spec.rb +9 -9
  64. data/lib/capybara/spec/session/{screenshot.rb → screenshot_spec.rb} +4 -2
  65. data/lib/capybara/spec/session/select_spec.rb +22 -22
  66. data/lib/capybara/spec/session/text_spec.rb +15 -10
  67. data/lib/capybara/spec/session/title_spec.rb +2 -2
  68. data/lib/capybara/spec/session/uncheck_spec.rb +7 -7
  69. data/lib/capybara/spec/session/unselect_spec.rb +14 -14
  70. data/lib/capybara/spec/session/visit_spec.rb +24 -17
  71. data/lib/capybara/spec/session/window/become_closed_spec.rb +84 -0
  72. data/lib/capybara/spec/session/window/current_window_spec.rb +25 -0
  73. data/lib/capybara/spec/session/window/open_new_window_spec.rb +28 -0
  74. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +114 -0
  75. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +83 -0
  76. data/lib/capybara/spec/session/window/window_spec.rb +141 -0
  77. data/lib/capybara/spec/session/window/windows_spec.rb +31 -0
  78. data/lib/capybara/spec/session/window/within_window_spec.rb +188 -0
  79. data/lib/capybara/spec/session/within_frame_spec.rb +9 -9
  80. data/lib/capybara/spec/session/within_spec.rb +16 -16
  81. data/lib/capybara/spec/spec_helper.rb +14 -4
  82. data/lib/capybara/spec/views/form.erb +7 -0
  83. data/lib/capybara/spec/views/popup_one.erb +1 -1
  84. data/lib/capybara/spec/views/popup_two.erb +1 -1
  85. data/lib/capybara/spec/views/with_js.erb +2 -0
  86. data/lib/capybara/spec/views/with_windows.erb +38 -0
  87. data/lib/capybara/version.rb +1 -1
  88. data/lib/capybara/window.rb +123 -0
  89. data/spec/basic_node_spec.rb +32 -32
  90. data/spec/capybara_spec.rb +6 -7
  91. data/spec/dsl_spec.rb +48 -48
  92. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  93. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  94. data/spec/rack_test_spec.rb +33 -19
  95. data/spec/result_spec.rb +13 -13
  96. data/spec/rspec/features_spec.rb +20 -15
  97. data/spec/rspec/matchers_spec.rb +109 -109
  98. data/spec/rspec_spec.rb +10 -10
  99. data/spec/selenium_spec.rb +31 -6
  100. data/spec/selenium_spec_chrome.rb +2 -2
  101. data/spec/server_spec.rb +13 -13
  102. metadata +51 -62
  103. checksums.yaml.gz.sig +0 -0
  104. data.tar.gz.sig +0 -0
  105. data/lib/capybara/spec/session/within_window_spec.rb +0 -45
  106. data/lib/capybara/spec/views/within_popups.erb +0 -25
  107. metadata.gz.sig +0 -0
@@ -68,7 +68,11 @@ module Capybara
68
68
  #
69
69
  # page.assert_selector('p#foo', :count => 4)
70
70
  #
71
- # This will check if the expression occurs exactly 4 times.
71
+ # This will check if the expression occurs exactly 4 times. See
72
+ # {Capybara::Node::Finders#all} for other available result size options.
73
+ #
74
+ # If a :count of 0 is specified, it will behave like {#assert_no_selector};
75
+ # however, use of that method is preferred over this one.
72
76
  #
73
77
  # It also accepts all options that {Capybara::Node::Finders#all} accepts,
74
78
  # such as :text and :visible.
@@ -88,7 +92,7 @@ module Capybara
88
92
  query = Capybara::Query.new(*args)
89
93
  synchronize(query.wait) do
90
94
  result = all(*args)
91
- result.matches_count? or raise Capybara::ExpectationNotMet, result.failure_message
95
+ raise Capybara::ExpectationNotMet, result.failure_message if result.size == 0 && !Capybara::Helpers.expects_none?(query.options)
92
96
  end
93
97
  return true
94
98
  end
@@ -98,18 +102,35 @@ module Capybara
98
102
  # Asserts that a given selector is not on the page or current node.
99
103
  # Usage is identical to Capybara::Node::Matchers#assert_selector
100
104
  #
105
+ # Query options such as :count, :minimum, :maximum, and :between are
106
+ # considered to be an integral part of the selector. This will return
107
+ # true, for example, if a page contains 4 anchors but the query expects 5:
108
+ #
109
+ # page.assert_no_selector('a', :minimum => 1) # Found, raises Capybara::ExpectationNotMet
110
+ # page.assert_no_selector('a', :count => 4) # Found, raises Capybara::ExpectationNotMet
111
+ # page.assert_no_selector('a', :count => 5) # Not Found, returns true
112
+ #
101
113
  # @param (see Capybara::Node::Finders#assert_selector)
102
114
  # @raise [Capybara::ExpectationNotMet] If the selector exists
103
115
  #
104
116
  def assert_no_selector(*args)
105
117
  query = Capybara::Query.new(*args)
106
118
  synchronize(query.wait) do
107
- result = all(*args)
108
- result.matches_count? and raise Capybara::ExpectationNotMet, result.negative_failure_message
119
+ begin
120
+ result = all(*args)
121
+ rescue Capybara::ExpectationNotMet => e
122
+ return true
123
+ else
124
+ if result.size > 0 || (result.size == 0 && Capybara::Helpers.expects_none?(query.options))
125
+ raise(Capybara::ExpectationNotMet, result.negative_failure_message)
126
+ end
127
+ end
109
128
  end
110
129
  return true
111
130
  end
112
131
 
132
+ alias_method :refute_selector, :assert_no_selector
133
+
113
134
  ##
114
135
  #
115
136
  # Checks if a given XPath expression is on the page or current node.
@@ -469,7 +490,7 @@ module Capybara
469
490
  content, options = args
470
491
  count = Capybara::Helpers.normalize_whitespace(text(type)).scan(Capybara::Helpers.to_regexp(content)).count
471
492
 
472
- Capybara::Helpers.matches_count?(count, options || {})
493
+ Capybara::Helpers.matches_count?(count, {:minimum=>1}.merge(options || {}))
473
494
  end
474
495
  end
475
496
  end
@@ -94,10 +94,17 @@ module Capybara
94
94
  # Whether or not the element is visible. Does not support CSS, so
95
95
  # the result may be inaccurate.
96
96
  #
97
+ # @param [Boolean] check_ancestors Whether to inherit visibility from ancestors
97
98
  # @return [Boolean] Whether the element is visible
98
99
  #
99
- def visible?
100
- native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or @hidden or name()='script' or name()='head']").size == 0
100
+ def visible?(check_ancestors = true)
101
+ if check_ancestors
102
+ #check size because oldest supported nokogiri doesnt support xpath boolean() function
103
+ native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or @hidden or name()='script' or name()='head']").size() == 0
104
+ else
105
+ #no need for an xpath if only checking the current element
106
+ !(native.has_attribute?('hidden') || (native[:style] =~ /display:\s?none/) || %w(script head).include?(tag_name))
107
+ end
101
108
  end
102
109
 
103
110
  ##
@@ -1,4 +1,6 @@
1
- class Capybara::RackTest::CSSHandlers
1
+ class Capybara::RackTest::CSSHandlers < BasicObject
2
+ include ::Kernel
3
+
2
4
  def disabled list
3
5
  list.find_all { |node| node.has_attribute? 'disabled' }
4
6
  end
@@ -1,6 +1,6 @@
1
1
  class Capybara::RackTest::Form < Capybara::RackTest::Node
2
2
  # This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for
3
- # the class specifically when determing whether to consturct the request as multipart.
3
+ # the class specifically when determining whether to construct the request as multipart.
4
4
  # That check should be based solely on the form element's 'enctype' attribute value,
5
5
  # which should probably be provided to Rack::Test in its non-GET request methods.
6
6
  class NilUploadedFile < Rack::Test::UploadedFile
@@ -71,7 +71,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
71
71
  end
72
72
 
73
73
  def submit(button)
74
- driver.submit(method, native['action'].to_s, params(button))
74
+ action = (button && button['formaction']) || native['action']
75
+ driver.submit(method, action.to_s, params(button))
75
76
  end
76
77
 
77
78
  def multipart?
@@ -99,14 +99,14 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
99
99
 
100
100
  protected
101
101
 
102
- def unnormalized_text
103
- if !visible?
102
+ def unnormalized_text(check_ancestor_visibility = true)
103
+ if !string_node.visible?(check_ancestor_visibility)
104
104
  ''
105
105
  elsif native.text?
106
106
  native.text
107
107
  elsif native.element?
108
108
  native.children.map do |child|
109
- Capybara::RackTest::Node.new(driver, child).unnormalized_text
109
+ Capybara::RackTest::Node.new(driver, child).unnormalized_text(false)
110
110
  end.join
111
111
  else
112
112
  ''
@@ -29,3 +29,4 @@ RSpec.configure do |config|
29
29
  end
30
30
  end
31
31
  end
32
+
@@ -21,7 +21,8 @@ def self.feature(*args, &block)
21
21
  options[:caller] ||= caller
22
22
  args.push(options)
23
23
 
24
- describe(*args, &block)
24
+ #call describe on RSpec in case user has expose_dsl_globally set to false
25
+ RSpec.describe(*args, &block)
25
26
  end
26
27
 
27
28
  RSpec.configuration.include Capybara::Features, :capybara_feature => true
@@ -51,16 +51,20 @@ module Capybara
51
51
  @actual.has_no_text?(type, content, options)
52
52
  end
53
53
 
54
- def failure_message_for_should
54
+ def failure_message
55
55
  message = Capybara::Helpers.failure_message(description, options)
56
56
  message << " in #{format(@actual.text(type))}"
57
57
  message
58
58
  end
59
59
 
60
- def failure_message_for_should_not
61
- failure_message_for_should.sub(/(to find)/, 'not \1')
60
+ def failure_message_when_negated
61
+ failure_message.sub(/(to find)/, 'not \1')
62
62
  end
63
63
 
64
+ # RSpec 2 compatibility:
65
+ alias_method :failure_message_for_should, :failure_message
66
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
67
+
64
68
  def description
65
69
  "text #{format(content)}"
66
70
  end
@@ -88,19 +92,50 @@ module Capybara
88
92
  @actual.has_no_title?(title)
89
93
  end
90
94
 
91
- def failure_message_for_should
95
+ def failure_message
92
96
  "expected there to be title #{title.inspect} in #{@actual.title.inspect}"
93
97
  end
94
98
 
95
- def failure_message_for_should_not
99
+ def failure_message_when_negated
96
100
  "expected there not to be title #{title.inspect} in #{@actual.title.inspect}"
97
101
  end
98
102
 
103
+ # RSpec 2 compatibility:
104
+ alias_method :failure_message_for_should, :failure_message
105
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
106
+
99
107
  def description
100
108
  "have title #{title.inspect}"
101
109
  end
102
110
  end
103
111
 
112
+ class BecomeClosed
113
+ def initialize(options)
114
+ @wait_time = Capybara::Query.new(options).wait
115
+ end
116
+
117
+ def matches?(window)
118
+ @window = window
119
+ start_time = Time.now
120
+ while window.exists? && (Time.now - start_time) < @wait_time
121
+ sleep 0.05
122
+ end
123
+ window.closed?
124
+ end
125
+
126
+ def failure_message
127
+ "expected #{@window.inspect} to become closed after #{@wait_time} seconds"
128
+ end
129
+
130
+ def failure_message_when_negated
131
+ "expected #{@window.inspect} not to become closed after #{@wait_time} seconds"
132
+ end
133
+
134
+ # RSpec 2 compatibility:
135
+ alias_method :failure_message_for_should, :failure_message
136
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
137
+ end
138
+
104
139
  def have_selector(*args)
105
140
  HaveSelector.new(*args)
106
141
  end
@@ -149,5 +184,15 @@ module Capybara
149
184
  def have_table(locator, options={})
150
185
  HaveSelector.new(:table, locator, options)
151
186
  end
187
+
188
+ ##
189
+ # Wait for window to become closed.
190
+ # @example
191
+ # expect(window).to become_closed(wait: 0.8)
192
+ # @param options [Hash] optional param
193
+ # @option options [Numeric] :wait (Capybara.default_wait_time) wait time
194
+ def become_closed(options = {})
195
+ BecomeClosed.new(options)
196
+ end
152
197
  end
153
198
  end
@@ -126,34 +126,98 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
126
126
  @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
127
127
  end
128
128
 
129
- def find_window( selector )
129
+ def current_window_handle
130
+ browser.window_handle
131
+ end
132
+
133
+ def window_size(handle)
134
+ within_given_window(handle) do
135
+ size = browser.manage.window.size
136
+ [size.width, size.height]
137
+ end
138
+ end
139
+
140
+ def resize_window_to(handle, width, height)
141
+ within_given_window(handle) do
142
+ browser.manage.window.resize_to(width, height)
143
+ end
144
+ end
145
+
146
+ def maximize_window(handle)
147
+ within_given_window(handle) do
148
+ browser.manage.window.maximize
149
+ end
150
+ sleep 0.1 # work around for https://code.google.com/p/selenium/issues/detail?id=7405
151
+ end
152
+
153
+ def close_window(handle)
154
+ within_given_window(handle) do
155
+ browser.close
156
+ end
157
+ end
158
+
159
+ def window_handles
160
+ browser.window_handles
161
+ end
162
+
163
+ def open_new_window
164
+ browser.execute_script('window.open();')
165
+ end
166
+
167
+ def switch_to_window(handle)
168
+ browser.switch_to.window handle
169
+ end
170
+
171
+ # @api private
172
+ def find_window(locator)
173
+ handles = browser.window_handles
174
+ return locator if handles.include? locator
175
+
130
176
  original_handle = browser.window_handle
131
- browser.window_handles.each do |handle|
132
- browser.switch_to.window handle
133
- if( selector == browser.execute_script("return window.name") ||
134
- browser.title.include?(selector) ||
135
- browser.current_url.include?(selector) ||
136
- (selector == handle) )
137
- browser.switch_to.window original_handle
177
+ handles.each do |handle|
178
+ switch_to_window(handle)
179
+ if (locator == browser.execute_script("return window.name") ||
180
+ browser.title.include?(locator) ||
181
+ browser.current_url.include?(locator))
182
+ switch_to_window(original_handle)
138
183
  return handle
139
184
  end
140
185
  end
141
- raise Capybara::ElementNotFound, "Could not find a window identified by #{selector}"
186
+ raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
142
187
  end
143
188
 
144
- def within_window(selector, &blk)
145
- handle = find_window( selector )
146
- browser.switch_to.window(handle, &blk)
189
+ def within_window(locator)
190
+ handle = find_window(locator)
191
+ browser.switch_to.window(handle) { yield }
147
192
  end
148
193
 
149
194
  def quit
150
195
  @browser.quit if @browser
151
196
  rescue Errno::ECONNREFUSED
152
197
  # Browser must have already gone
198
+ ensure
199
+ @browser = nil
153
200
  end
154
201
 
155
202
  def invalid_element_errors
156
203
  [Selenium::WebDriver::Error::StaleElementReferenceError, Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::ElementNotVisibleError]
157
204
  end
158
205
 
206
+ def no_such_window_error
207
+ Selenium::WebDriver::Error::NoSuchWindowError
208
+ end
209
+
210
+ private
211
+
212
+ def within_given_window(handle)
213
+ original_handle = self.current_window_handle
214
+ if handle == original_handle
215
+ yield
216
+ else
217
+ switch_to_window(handle)
218
+ result = yield
219
+ switch_to_window(original_handle)
220
+ result
221
+ end
222
+ end
159
223
  end
@@ -70,6 +70,14 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
70
70
  def click
71
71
  native.click
72
72
  end
73
+
74
+ def right_click
75
+ driver.browser.action.context_click(native).perform
76
+ end
77
+
78
+ def double_click
79
+ driver.browser.action.double_click(native).perform
80
+ end
73
81
 
74
82
  def hover
75
83
  driver.browser.action.move_to(native).perform
@@ -36,7 +36,7 @@ module Capybara
36
36
  def initialize(app, port=Capybara.server_port, host=Capybara.server_host)
37
37
  @app = app
38
38
  @middleware = Middleware.new(@app)
39
- @server_thread = nil # supress warnings
39
+ @server_thread = nil # suppress warnings
40
40
  @host, @port = host, port
41
41
  @port ||= Capybara::Server.ports[@app.object_id]
42
42
  @port ||= find_available_port
@@ -19,7 +19,7 @@ module Capybara
19
19
  #
20
20
  # session.fill_in('q', :with => 'Capybara')
21
21
  # session.click_button('Search')
22
- # session.should have_content('Capybara')
22
+ # expect(session).to have_content('Capybara')
23
23
  #
24
24
  # When using capybara/dsl, the Session is initialized automatically for you.
25
25
  #
@@ -34,15 +34,17 @@ module Capybara
34
34
  :has_no_field?, :has_checked_field?, :has_unchecked_field?,
35
35
  :has_no_table?, :has_table?, :unselect, :has_select?, :has_no_select?,
36
36
  :has_selector?, :has_no_selector?, :click_on, :has_no_checked_field?,
37
- :has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector
37
+ :has_no_unchecked_field?, :query, :assert_selector, :assert_no_selector,
38
+ :refute_selector
38
39
  ]
39
40
  SESSION_METHODS = [
40
41
  :body, :html, :source, :current_url, :current_host, :current_path,
41
42
  :execute_script, :evaluate_script, :visit, :go_back, :go_forward,
42
- :within, :within_fieldset, :within_table, :within_frame, :within_window,
43
+ :within, :within_fieldset, :within_table, :within_frame, :current_window,
44
+ :windows, :open_new_window, :switch_to_window, :within_window, :window_opened_by,
43
45
  :save_page, :save_and_open_page, :save_screenshot,
44
- :reset_session!, :response_headers, :status_code,
45
- :title, :has_title?, :has_no_title?, :current_scope
46
+ :save_and_open_screenshot, :reset_session!, :response_headers,
47
+ :status_code, :title, :has_title?, :has_no_title?, :current_scope
46
48
  ]
47
49
  DSL_METHODS = NODE_METHODS + SESSION_METHODS
48
50
 
@@ -72,20 +74,42 @@ module Capybara
72
74
 
73
75
  ##
74
76
  #
75
- # Reset the session, removing all cookies.
77
+ # Reset the session (i.e. remove cookies and navigate to blank page)
78
+ #
79
+ # This method does not:
80
+ #
81
+ # * accept modal dialogs if they are present
82
+ # * clear browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc.
83
+ # * modify state of the driver/underlying browser in any other way
84
+ #
85
+ # as doing so will result in performance downsides and it's not needed to do everything from the list above for most apps.
86
+ #
87
+ # If you want to do anything from the list above on a general basis you can:
88
+ #
89
+ # * write RSpec/Cucumber/etc. after hook
90
+ # * monkeypatch this method
91
+ # * use Ruby's `prepend` method
76
92
  #
77
93
  def reset!
78
94
  if @touched
79
95
  driver.reset!
80
- @touched = false
81
96
  assert_no_selector :xpath, "/html/body/*"
97
+ @touched = false
82
98
  end
99
+ raise_server_error!
100
+ end
101
+ alias_method :cleanup!, :reset!
102
+ alias_method :reset_session!, :reset!
103
+
104
+ ##
105
+ #
106
+ # Raise errors encountered in the server
107
+ #
108
+ def raise_server_error!
83
109
  raise @server.error if Capybara.raise_server_errors and @server and @server.error
84
110
  ensure
85
111
  @server.reset_error! if @server
86
112
  end
87
- alias_method :cleanup!, :reset!
88
- alias_method :reset_session!, :reset!
89
113
 
90
114
  ##
91
115
  #
@@ -178,6 +202,8 @@ module Capybara
178
202
  # @param [String] url The URL to navigate to
179
203
  #
180
204
  def visit(url)
205
+ raise_server_error!
206
+
181
207
  @touched = true
182
208
 
183
209
  if url !~ /^http/ and Capybara.app_host
@@ -302,17 +328,172 @@ module Capybara
302
328
  end
303
329
 
304
330
  ##
331
+ # @return [Capybara::Window] current window
305
332
  #
306
- # Execute the given block within the given window. Only works on
307
- # some drivers (e.g. Selenium)
333
+ def current_window
334
+ Window.new(self, driver.current_window_handle)
335
+ end
336
+
337
+ ##
338
+ # Get all opened windows.
339
+ # The order of windows in returned array is not defined.
340
+ # The driver may sort windows by their creation time but it's not required.
308
341
  #
309
- # @param [String] handle of the window
342
+ # @return [Array<Capybara::Window>] an array of all windows
310
343
  #
311
- def within_window(handle, &blk)
312
- scopes.push(nil)
313
- driver.within_window(handle, &blk)
314
- ensure
315
- scopes.pop
344
+ def windows
345
+ driver.window_handles.map do |handle|
346
+ Window.new(self, handle)
347
+ end
348
+ end
349
+
350
+ ##
351
+ # Open new window.
352
+ # Current window doesn't change as the result of this call.
353
+ # It should be switched to explicitly.
354
+ #
355
+ # @return [Capybara::Window] window that has been opened
356
+ #
357
+ def open_new_window
358
+ window_opened_by do
359
+ driver.open_new_window
360
+ end
361
+ end
362
+
363
+ ##
364
+ # @overload switch_to_window(&block)
365
+ # Switches to the first window for which given block returns a value other than false or nil.
366
+ # If window that matches block can't be found, the window will be switched back and `WindowError` will be raised.
367
+ # @example
368
+ # window = switch_to_window { title == 'Page title' }
369
+ # @raise [Capybara::WindowError] if no window matches given block
370
+ # @overload switch_to_window(window)
371
+ # @param window [Capybara::Window] window that should be switched to
372
+ # @raise [Capybara::Driver::Base#no_such_window_error] if unexistent (e.g. closed) window was passed
373
+ #
374
+ # @return [Capybara::Window] window that has been switched to
375
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
376
+ # `within_frame` or `within_window` methods
377
+ # @raise [ArgumentError] if both or neither arguments were provided
378
+ #
379
+ def switch_to_window(window = nil)
380
+ block_given = block_given?
381
+ if window && block_given
382
+ raise ArgumentError, "`switch_to_window` can take either a block or a window, not both"
383
+ elsif !window && !block_given
384
+ raise ArgumentError, "`switch_to_window`: either window or block should be provided"
385
+ elsif scopes.size > 1
386
+ raise Capybara::ScopeError, "`switch_to_window` is not supposed to be invoked from "\
387
+ "`within`'s, `within_frame`'s' or `within_window`'s' block."
388
+ end
389
+
390
+ if window
391
+ driver.switch_to_window(window.handle)
392
+ window
393
+ else
394
+ original_window_handle = driver.current_window_handle
395
+ begin
396
+ driver.window_handles.each do |handle|
397
+ driver.switch_to_window handle
398
+ if yield
399
+ return Window.new(self, handle)
400
+ end
401
+ end
402
+ rescue => e
403
+ driver.switch_to_window(original_window_handle)
404
+ raise e
405
+ else
406
+ driver.switch_to_window(original_window_handle)
407
+ raise Capybara::WindowError, "Could not find a window matching block/lambda"
408
+ end
409
+ end
410
+ end
411
+
412
+ ##
413
+ # This method does the following:
414
+ #
415
+ # 1. Switches to the given window (it can be located by window instance/lambda/string).
416
+ # 2. Executes the given block (within window located at previous step).
417
+ # 3. Switches back (this step will be invoked even if exception will happen at second step)
418
+ #
419
+ # @overload within_window(window) { do_something }
420
+ # @param window [Capybara::Window] instance of `Capybara::Window` class
421
+ # that will be switched to
422
+ # @raise [driver#no_such_window_error] if unexistent (e.g. closed) window was passed
423
+ # @overload within_window(proc_or_lambda) { do_something }
424
+ # @param lambda [Proc] lambda. First window for which lambda
425
+ # returns a value other than false or nil will be switched to.
426
+ # @example
427
+ # within_window(->{ page.title == 'Page title' }) { click_button 'Submit' }
428
+ # @raise [Capybara::WindowError] if no window matching lambda was found
429
+ # @overload within_window(string) { do_something }
430
+ # @deprecated Pass window or lambda instead
431
+ # @param [String] handle, name, url or title of the window
432
+ #
433
+ # @raise [Capybara::ScopeError] if this method is invoked inside `within`,
434
+ # `within_frame` or `within_window` methods
435
+ # @return value returned by the block
436
+ #
437
+ def within_window(window_or_handle)
438
+ if window_or_handle.instance_of?(Capybara::Window)
439
+ original = current_window
440
+ switch_to_window(window_or_handle) unless original == window_or_handle
441
+ scopes << nil
442
+ begin
443
+ yield
444
+ ensure
445
+ @scopes.pop
446
+ switch_to_window(original) unless original == window_or_handle
447
+ end
448
+ elsif window_or_handle.is_a?(Proc)
449
+ original = current_window
450
+ switch_to_window { window_or_handle.call }
451
+ scopes << nil
452
+ begin
453
+ yield
454
+ ensure
455
+ @scopes.pop
456
+ switch_to_window(original)
457
+ end
458
+ else
459
+ offending_line = caller.first
460
+ file_line = offending_line.match(/^(.+?):(\d+)/)[0]
461
+ warn "DEPRECATION WARNING: Passing string argument to #within_window is deprecated. "\
462
+ "Pass window object or lambda. (called from #{file_line})"
463
+ begin
464
+ scopes << nil
465
+ driver.within_window(window_or_handle) { yield }
466
+ ensure
467
+ @scopes.pop
468
+ end
469
+ end
470
+ end
471
+
472
+ ##
473
+ # Get the window that has been opened by the passed block.
474
+ # It will wait for it to be opened (in the same way as other Capybara methods wait).
475
+ # It's better to use this method than `windows.last`
476
+ # {https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10 as order of windows isn't defined in some drivers}
477
+ #
478
+ # @param options [Hash]
479
+ # @option options [Numeric] :wait (Capybara.default_wait_time) wait time
480
+ # @return [Capybara::Window] the window that has been opened within a block
481
+ # @raise [Capybara::WindowError] if block passed to window hasn't opened window
482
+ # or opened more than one window
483
+ #
484
+ def window_opened_by(options = {}, &block)
485
+ old_handles = driver.window_handles
486
+ block.call
487
+
488
+ wait_time = Capybara::Query.new(options).wait
489
+ document.synchronize(wait_time, errors: [Capybara::WindowError]) do
490
+ opened_handles = (driver.window_handles - old_handles)
491
+ if opened_handles.size != 1
492
+ raise Capybara::WindowError, "block passed to #window_opened_by "\
493
+ "opened #{opened_handles.size} windows instead of 1"
494
+ end
495
+ Window.new(self, opened_handles.first)
496
+ end
316
497
  end
317
498
 
318
499
  ##
@@ -349,12 +530,11 @@ module Capybara
349
530
  # @param [String] path The path to where it should be saved [optional]
350
531
  #
351
532
  def save_page(path=nil)
352
- path ||= "capybara-#{Time.new.strftime("%Y%m%d%H%M%S")}#{rand(10**10)}.html"
353
- path = File.expand_path(path, Capybara.save_and_open_page_path)
533
+ path ||= default_path('html')
354
534
 
355
535
  FileUtils.mkdir_p(File.dirname(path))
356
536
 
357
- File.open(path,'w') { |f| f.write(Capybara::Helpers.inject_asset_host(body)) }
537
+ File.open(path,'wb') { |f| f.write(Capybara::Helpers.inject_asset_host(body)) }
358
538
  path
359
539
  end
360
540
 
@@ -366,14 +546,7 @@ module Capybara
366
546
  #
367
547
  def save_and_open_page(file_name=nil)
368
548
  file_name = save_page(file_name)
369
-
370
- begin
371
- require "launchy"
372
- Launchy.open(file_name)
373
- rescue LoadError
374
- warn "Page saved to #{file_name} with save_and_open_page."
375
- warn "Please install the launchy gem to open page automatically."
376
- end
549
+ open_file(file_name)
377
550
  end
378
551
 
379
552
  ##
@@ -383,7 +556,23 @@ module Capybara
383
556
  # @param [String] path A string of image path
384
557
  # @option [Hash] options Options for saving screenshot
385
558
  def save_screenshot(path, options={})
559
+ path ||= default_path('png')
560
+
561
+ FileUtils.mkdir_p(File.dirname(path))
562
+
386
563
  driver.save_screenshot(path, options)
564
+ path
565
+ end
566
+
567
+ ##
568
+ #
569
+ # Save a screenshot of the page and open it for inspection
570
+ #
571
+ # @param [String] file_name The path to where it should be saved [optional]
572
+ #
573
+ def save_and_open_screenshot(file_name=nil)
574
+ file_name = save_screenshot(file_name)
575
+ open_file(file_name)
387
576
  end
388
577
 
389
578
  def document
@@ -429,8 +618,24 @@ module Capybara
429
618
 
430
619
  private
431
620
 
621
+ def open_file(file_name)
622
+ begin
623
+ require "launchy"
624
+ Launchy.open(file_name)
625
+ rescue LoadError
626
+ warn "File saved to #{file_name}."
627
+ warn "Please install the launchy gem to open the file automatically."
628
+ end
629
+ end
630
+
631
+ def default_path(extension)
632
+ timestamp = Time.new.strftime("%Y%m%d%H%M%S")
633
+ path = "capybara-#{timestamp}#{rand(10**10)}.#{extension}"
634
+ File.expand_path(path, Capybara.save_and_open_page_path)
635
+ end
636
+
432
637
  def scopes
433
- @scopes ||= [document]
638
+ @scopes ||= [nil]
434
639
  end
435
640
  end
436
641
  end