capybara 2.2.1 → 2.3.0

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