capybara 2.4.4 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +32 -5
  3. data/README.md +69 -8
  4. data/lib/capybara.rb +50 -29
  5. data/lib/capybara/driver/base.rb +4 -0
  6. data/lib/capybara/driver/node.rb +4 -0
  7. data/lib/capybara/helpers.rb +17 -5
  8. data/lib/capybara/node/actions.rb +16 -11
  9. data/lib/capybara/node/base.rb +7 -7
  10. data/lib/capybara/node/document_matchers.rb +1 -1
  11. data/lib/capybara/node/element.rb +82 -7
  12. data/lib/capybara/node/finders.rb +62 -22
  13. data/lib/capybara/node/matchers.rb +3 -3
  14. data/lib/capybara/node/simple.rb +6 -1
  15. data/lib/capybara/queries/base_query.rb +1 -1
  16. data/lib/capybara/queries/current_path_query.rb +58 -0
  17. data/lib/capybara/queries/text_query.rb +2 -11
  18. data/lib/capybara/rack_test/browser.rb +7 -2
  19. data/lib/capybara/rack_test/driver.rb +4 -0
  20. data/lib/capybara/rack_test/form.rb +2 -1
  21. data/lib/capybara/rack_test/node.rb +1 -0
  22. data/lib/capybara/result.rb +2 -2
  23. data/lib/capybara/rspec.rb +1 -0
  24. data/lib/capybara/rspec/features.rb +1 -1
  25. data/lib/capybara/rspec/matchers.rb +42 -3
  26. data/lib/capybara/selector.rb +7 -2
  27. data/lib/capybara/selenium/driver.rb +26 -12
  28. data/lib/capybara/selenium/node.rb +42 -6
  29. data/lib/capybara/server.rb +1 -1
  30. data/lib/capybara/session.rb +78 -50
  31. data/lib/capybara/session/matchers.rb +69 -0
  32. data/lib/capybara/spec/public/test.js +8 -0
  33. data/lib/capybara/spec/session/all_spec.rb +5 -0
  34. data/lib/capybara/spec/session/assert_current_path.rb +59 -0
  35. data/lib/capybara/spec/session/assert_text.rb +1 -1
  36. data/lib/capybara/spec/session/attach_file_spec.rb +2 -2
  37. data/lib/capybara/spec/session/body_spec.rb +2 -0
  38. data/lib/capybara/spec/session/click_button_spec.rb +17 -8
  39. data/lib/capybara/spec/session/click_link_spec.rb +32 -1
  40. data/lib/capybara/spec/session/current_url_spec.rb +5 -0
  41. data/lib/capybara/spec/session/fill_in_spec.rb +1 -1
  42. data/lib/capybara/spec/session/find_field_spec.rb +17 -0
  43. data/lib/capybara/spec/session/find_spec.rb +14 -5
  44. data/lib/capybara/spec/session/first_spec.rb +24 -0
  45. data/lib/capybara/spec/session/has_current_path_spec.rb +68 -0
  46. data/lib/capybara/spec/session/has_link_spec.rb +3 -0
  47. data/lib/capybara/spec/session/has_text_spec.rb +7 -0
  48. data/lib/capybara/spec/session/node_spec.rb +45 -6
  49. data/lib/capybara/spec/session/reset_session_spec.rb +18 -1
  50. data/lib/capybara/spec/session/save_and_open_page_spec.rb +19 -0
  51. data/lib/capybara/spec/session/save_page_spec.rb +12 -3
  52. data/lib/capybara/spec/session/save_screenshot_spec.rb +23 -0
  53. data/lib/capybara/spec/session/select_spec.rb +12 -0
  54. data/lib/capybara/spec/session/title_spec.rb +2 -2
  55. data/lib/capybara/spec/session/window/become_closed_spec.rb +4 -4
  56. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +8 -0
  57. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +14 -8
  58. data/lib/capybara/spec/session/window/window_spec.rb +24 -4
  59. data/lib/capybara/spec/spec_helper.rb +3 -1
  60. data/lib/capybara/spec/test_app.rb +10 -1
  61. data/lib/capybara/spec/views/form.erb +7 -1
  62. data/lib/capybara/spec/views/path.erb +12 -0
  63. data/lib/capybara/spec/views/with_html.erb +2 -0
  64. data/lib/capybara/spec/views/with_js.erb +9 -1
  65. data/lib/capybara/spec/views/with_title.erb +4 -1
  66. data/lib/capybara/spec/views/with_windows.erb +2 -2
  67. data/lib/capybara/version.rb +1 -1
  68. data/spec/basic_node_spec.rb +1 -0
  69. data/spec/capybara_spec.rb +12 -3
  70. data/spec/dsl_spec.rb +18 -6
  71. data/spec/rack_test_spec.rb +6 -5
  72. data/spec/rspec/matchers_spec.rb +62 -16
  73. data/spec/rspec/views_spec.rb +7 -0
  74. data/spec/selenium_spec.rb +38 -3
  75. data/spec/selenium_spec_chrome.rb +3 -7
  76. metadata +13 -4
@@ -0,0 +1,58 @@
1
+ module Capybara
2
+ # @api private
3
+ module Queries
4
+ class CurrentPathQuery < BaseQuery
5
+ def initialize(expected_path, options = {})
6
+ @expected_path = expected_path
7
+ @options = {
8
+ url: false,
9
+ only_path: false }.merge(options)
10
+ assert_valid_keys
11
+ end
12
+
13
+ def resolves_for?(session)
14
+ @actual_path = if options[:url]
15
+ session.current_url
16
+ else
17
+ if options[:only_path]
18
+ URI.parse(session.current_url).path
19
+ else
20
+ URI.parse(session.current_url).request_uri
21
+ end
22
+ end
23
+
24
+ if @expected_path.is_a? Regexp
25
+ @actual_path.match(@expected_path)
26
+ else
27
+ @expected_path == @actual_path
28
+ end
29
+ end
30
+
31
+ def failure_message
32
+ failure_message_helper
33
+ end
34
+
35
+ def negative_failure_message
36
+ failure_message_helper(' not')
37
+ end
38
+
39
+ private
40
+
41
+ def failure_message_helper(negated = '')
42
+ verb = (@expected_path.is_a?(Regexp))? 'match' : 'equal'
43
+ "expected #{@actual_path.inspect}#{negated} to #{verb} #{@expected_path.inspect}"
44
+ end
45
+
46
+ def valid_keys
47
+ [:wait, :url, :only_path]
48
+ end
49
+
50
+ def assert_valid_keys
51
+ super
52
+ if options[:url] && options[:only_path]
53
+ raise ArgumentError, "the :url and :only_path options cannot both be true"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -3,7 +3,7 @@ module Capybara
3
3
  module Queries
4
4
  class TextQuery < BaseQuery
5
5
  def initialize(*args)
6
- @type = args.shift if args.first.is_a?(Symbol) || args.first.nil?
6
+ @type = (args.first.is_a?(Symbol) || args.first.nil?) ? args.shift : nil
7
7
  @expected_text, @options = args
8
8
  unless @expected_text.is_a?(Regexp)
9
9
  @expected_text = Capybara::Helpers.normalize_whitespace(@expected_text)
@@ -11,15 +11,6 @@ module Capybara
11
11
  @search_regexp = Capybara::Helpers.to_regexp(@expected_text)
12
12
  @options ||= {}
13
13
  assert_valid_keys
14
-
15
- # this is needed to not break existing tests that may use keys supported by `Query` but not supported by `TextQuery`
16
- # can be removed in next minor version (> 2.4)
17
- invalid_keys = @options.keys - (COUNT_KEYS + [:wait])
18
- unless invalid_keys.empty?
19
- invalid_names = invalid_keys.map(&:inspect).join(", ")
20
- valid_names = valid_keys.map(&:inspect).join(", ")
21
- warn "invalid keys #{invalid_names}, should be one of #{valid_names}"
22
- end
23
14
  end
24
15
 
25
16
  def resolve_for(node)
@@ -49,7 +40,7 @@ module Capybara
49
40
  private
50
41
 
51
42
  def valid_keys
52
- Capybara::Query::VALID_KEYS # can be changed to COUNT_KEYS + [:wait] in next minor version (> 2.4)
43
+ COUNT_KEYS + [:wait]
53
44
  end
54
45
  end
55
46
  end
@@ -27,7 +27,7 @@ class Capybara::RackTest::Browser
27
27
  end
28
28
 
29
29
  def follow(method, path, attributes = {})
30
- return if path.gsub(/^#{request_path}/, '').start_with?('#')
30
+ return if path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
31
31
  process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
32
32
  end
33
33
 
@@ -96,7 +96,12 @@ class Capybara::RackTest::Browser
96
96
  end
97
97
 
98
98
  def title
99
- dom.xpath("//title").text
99
+ if dom.respond_to? :title
100
+ dom.title
101
+ else
102
+ #old versions of nokogiri don't have #title - remove in 3.0
103
+ dom.xpath('/html/head/title | /html/title').first.text
104
+ end
100
105
  end
101
106
 
102
107
  protected
@@ -86,6 +86,10 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
86
86
  @browser = nil
87
87
  end
88
88
 
89
+ def browser_initialized?
90
+ !@browser.nil?
91
+ end
92
+
89
93
  def get(*args, &block); browser.get(*args, &block); end
90
94
  def post(*args, &block); browser.post(*args, &block); end
91
95
  def put(*args, &block); browser.put(*args, &block); end
@@ -72,7 +72,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
72
72
 
73
73
  def submit(button)
74
74
  action = (button && button['formaction']) || native['action']
75
- driver.submit(method, action.to_s, params(button))
75
+ requset_method = (button && button['formmethod']) || method
76
+ driver.submit(requset_method, action.to_s, params(button))
76
77
  end
77
78
 
78
79
  def multipart?
@@ -36,6 +36,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
36
36
  end
37
37
 
38
38
  def select_option
39
+ return if disabled?
39
40
  if select_node['multiple'] != 'multiple'
40
41
  select_node.find_xpath(".//option[@selected]").each { |node| node.native.remove_attribute("selected") }
41
42
  end
@@ -3,7 +3,7 @@ require 'forwardable'
3
3
  module Capybara
4
4
 
5
5
  ##
6
- # A {Capybara::Result} represents a collection of {Capybara::Element} on the page. It is possible to interact with this
6
+ # A {Capybara::Result} represents a collection of {Capybara::Node::Element} on the page. It is possible to interact with this
7
7
  # collection similar to an Array because it implements Enumerable and offers the following Array methods through delegation:
8
8
  #
9
9
  # * []
@@ -16,7 +16,7 @@ module Capybara
16
16
  # * last()
17
17
  # * empty?()
18
18
  #
19
- # @see Capybara::Element
19
+ # @see Capybara::Node::Element
20
20
  #
21
21
  class Result
22
22
  include Enumerable
@@ -7,6 +7,7 @@ require 'capybara/rspec/features'
7
7
  RSpec.configure do |config|
8
8
  config.include Capybara::DSL, :type => :feature
9
9
  config.include Capybara::RSpecMatchers, :type => :feature
10
+ config.include Capybara::RSpecMatchers, :type => :view
10
11
 
11
12
  # A work-around to support accessing the current example that works in both
12
13
  # RSpec 2 and RSpec 3.
@@ -11,7 +11,7 @@ if RSpec::Core::Version::STRING.to_f >= 3.0
11
11
  config.alias_example_group_to :feature, :capybara_feature => true, :type => :feature
12
12
  config.alias_example_to :scenario
13
13
  config.alias_example_to :xscenario, :skip => "Temporarily disabled with xscenario"
14
- # config.alias_example_to :fscenario, :focus => true
14
+ config.alias_example_to :fscenario, :focus => true
15
15
  end
16
16
  else
17
17
  module Capybara
@@ -123,6 +123,41 @@ module Capybara
123
123
  alias_method :failure_message_for_should_not, :failure_message_when_negated
124
124
  end
125
125
 
126
+ class HaveCurrentPath < Matcher
127
+ attr_reader :current_path
128
+
129
+ attr_reader :failure_message, :failure_message_when_negated
130
+
131
+ def initialize(*args)
132
+ @args = args
133
+
134
+ # are set just for backwards compatability
135
+ @current_path = args.first
136
+ end
137
+
138
+ def matches?(actual)
139
+ wrap(actual).assert_current_path(*@args)
140
+ rescue Capybara::ExpectationNotMet => e
141
+ @failure_message = e.message
142
+ return false
143
+ end
144
+
145
+ def does_not_match?(actual)
146
+ wrap(actual).assert_no_current_path(*@args)
147
+ rescue Capybara::ExpectationNotMet => e
148
+ @failure_message_when_negated = e.message
149
+ return false
150
+ end
151
+
152
+ def description
153
+ "have current path #{current_path.inspect}"
154
+ end
155
+
156
+ # RSpec 2 compatibility:
157
+ alias_method :failure_message_for_should, :failure_message
158
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
159
+ end
160
+
126
161
  class BecomeClosed
127
162
  def initialize(options)
128
163
  @wait_time = Capybara::Query.new(options).wait
@@ -130,9 +165,9 @@ module Capybara
130
165
 
131
166
  def matches?(window)
132
167
  @window = window
133
- start_time = Time.now
168
+ start_time = Capybara::Helpers.monotonic_time
134
169
  while window.exists?
135
- return false if (Time.now - start_time) > @wait_time
170
+ return false if (Capybara::Helpers.monotonic_time - start_time) > @wait_time
136
171
  sleep 0.05
137
172
  end
138
173
  true
@@ -172,6 +207,10 @@ module Capybara
172
207
  HaveTitle.new(title, options)
173
208
  end
174
209
 
210
+ def have_current_path(path, options = {})
211
+ HaveCurrentPath.new(path, options)
212
+ end
213
+
175
214
  def have_link(locator, options={})
176
215
  HaveSelector.new(:link, locator, options)
177
216
  end
@@ -205,7 +244,7 @@ module Capybara
205
244
  # @example
206
245
  # expect(window).to become_closed(wait: 0.8)
207
246
  # @param options [Hash] optional param
208
- # @option options [Numeric] :wait (Capybara.default_wait_time) wait time
247
+ # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum wait time
209
248
  def become_closed(options = {})
210
249
  BecomeClosed.new(options)
211
250
  end
@@ -116,6 +116,7 @@ Capybara.add_selector(:field) do
116
116
  filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
117
117
  filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
118
118
  filter(:disabled, default: false, boolean: true) { |node, value| not(value ^ node.disabled?) }
119
+ filter(:readonly, boolean: true) { |node, value| not(value ^ node[:readonly]) }
119
120
  filter(:with) { |node, with| node.value == with.to_s }
120
121
  filter(:type) do |node, type|
121
122
  if ['textarea', 'select'].include?(type)
@@ -150,7 +151,11 @@ end
150
151
  Capybara.add_selector(:link) do
151
152
  xpath { |locator| XPath::HTML.link(locator) }
152
153
  filter(:href) do |node, href|
153
- node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)])
154
+ if href.is_a? Regexp
155
+ node[:href].match href
156
+ else
157
+ node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)], minimum: 0)
158
+ end
154
159
  end
155
160
  describe { |options| " with href #{options[:href].inspect}" if options[:href] }
156
161
  end
@@ -210,7 +215,7 @@ Capybara.add_selector(:select) do
210
215
  actual = node.all(:xpath, './/option').map { |option| option.text }
211
216
  options.sort == actual.sort
212
217
  end
213
- filter(:with_options) { |node, options| options.all? { |option| node.first(:option, option) } }
218
+ filter(:with_options) { |node, options| options.all? { |option| node.first(:option, option, minimum: 0) } }
214
219
  filter(:selected) do |node, selected|
215
220
  actual = node.all(:xpath, './/option').select { |option| option.selected? }.map { |option| option.text }
216
221
  [selected].flatten.sort == actual.sort
@@ -116,7 +116,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
116
116
 
117
117
  ##
118
118
  #
119
- # Webdriver supports frame name, id, index(zero-based) or {Capybara::Element} to find iframe
119
+ # Webdriver supports frame name, id, index(zero-based) or {Capybara::Node::Element} to find iframe
120
120
  #
121
121
  # @overload within_frame(index)
122
122
  # @param [Integer] index index of a frame
@@ -126,17 +126,24 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
126
126
  # @param [Capybara::Node::Base] a_node frame element
127
127
  #
128
128
  def within_frame(frame_handle)
129
- @frame_handles[browser.window_handle] ||= []
130
129
  frame_handle = frame_handle.native if frame_handle.is_a?(Capybara::Node::Base)
131
- @frame_handles[browser.window_handle] << frame_handle
132
- a=browser.switch_to.frame(frame_handle)
130
+ if !browser.switch_to.respond_to?(:parent_frame)
131
+ # Selenium Webdriver < 2.43 doesnt support moving back to the parent
132
+ @frame_handles[browser.window_handle] ||= []
133
+ @frame_handles[browser.window_handle] << frame_handle
134
+ end
135
+ browser.switch_to.frame(frame_handle)
133
136
  yield
134
137
  ensure
135
- # There doesnt appear to be any way in Webdriver to move back to a parent frame
136
- # other than going back to the root and then reiterating down
137
- @frame_handles[browser.window_handle].pop
138
- browser.switch_to.default_content
139
- @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
138
+ if browser.switch_to.respond_to?(:parent_frame)
139
+ browser.switch_to.parent_frame
140
+ else
141
+ # There doesnt appear to be any way in Selenium Webdriver < 2.43 to move back to a parent frame
142
+ # other than going back to the root and then reiterating down
143
+ @frame_handles[browser.window_handle].pop
144
+ browser.switch_to.default_content
145
+ @frame_handles[browser.window_handle].each { |fh| browser.switch_to.frame(fh) }
146
+ end
140
147
  end
141
148
 
142
149
  def current_window_handle
@@ -230,13 +237,20 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
230
237
  end
231
238
 
232
239
  def invalid_element_errors
233
- [Selenium::WebDriver::Error::StaleElementReferenceError, Selenium::WebDriver::Error::UnhandledError, Selenium::WebDriver::Error::ElementNotVisibleError]
240
+ [Selenium::WebDriver::Error::StaleElementReferenceError,
241
+ Selenium::WebDriver::Error::UnhandledError,
242
+ Selenium::WebDriver::Error::ElementNotVisibleError,
243
+ Selenium::WebDriver::Error::InvalidSelectorError] # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
234
244
  end
235
245
 
236
246
  def no_such_window_error
237
247
  Selenium::WebDriver::Error::NoSuchWindowError
238
248
  end
239
249
 
250
+ def browser_initialized?
251
+ !@browser.nil?
252
+ end
253
+
240
254
  private
241
255
 
242
256
  def within_given_window(handle)
@@ -255,10 +269,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
255
269
  # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
256
270
  # Actual wait time may be longer than specified
257
271
  wait = Selenium::WebDriver::Wait.new(
258
- timeout: (options[:wait] || Capybara.default_wait_time),
272
+ timeout: (options[:wait] || Capybara.default_max_wait_time),
259
273
  ignore: Selenium::WebDriver::Error::NoAlertPresentError)
260
274
  begin
261
- modal = wait.until do
275
+ wait.until do
262
276
  alert = @browser.switch_to.alert
263
277
  regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
264
278
  alert.text.match(regexp) ? alert : nil
@@ -23,7 +23,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
23
23
  end
24
24
  end
25
25
 
26
- def set(value)
26
+ def set(value, fill_options={})
27
27
  tag_name = self.tag_name
28
28
  type = self[:type]
29
29
  if (Array === value) && !self[:multiple]
@@ -42,9 +42,17 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
42
42
  elsif value.to_s.empty?
43
43
  native.clear
44
44
  else
45
- #script can change a readonly element which user input cannot, so dont execute if readonly
46
- driver.browser.execute_script "arguments[0].value = ''", native
47
- native.send_keys(value.to_s)
45
+ if fill_options[:clear] == :backspace
46
+ # Clear field by sending the correct number of backspace keys.
47
+ backspaces = [:backspace] * self.value.to_s.length
48
+ native.send_keys(*(backspaces + [value.to_s]))
49
+ else
50
+ # Clear field by JavaScript assignment of the value property.
51
+ # Script can change a readonly element which user input cannot, so
52
+ # don't execute if readonly.
53
+ driver.browser.execute_script "arguments[0].value = ''", native
54
+ native.send_keys(value.to_s)
55
+ end
48
56
  end
49
57
  elsif native.attribute('isContentEditable')
50
58
  #ensure we are focused on the element
@@ -72,15 +80,19 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
72
80
  def click
73
81
  native.click
74
82
  end
75
-
83
+
76
84
  def right_click
77
85
  driver.browser.action.context_click(native).perform
78
86
  end
79
-
87
+
80
88
  def double_click
81
89
  driver.browser.action.double_click(native).perform
82
90
  end
83
91
 
92
+ def send_keys(*args)
93
+ native.send_keys(*args)
94
+ end
95
+
84
96
  def hover
85
97
  driver.browser.action.move_to(native).perform
86
98
  end
@@ -121,6 +133,30 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
121
133
  native == other.native
122
134
  end
123
135
 
136
+ def path
137
+ path = find_xpath('ancestor::*').reverse
138
+ path.unshift self
139
+
140
+ result = []
141
+ while node = path.shift
142
+ parent = path.first
143
+
144
+ if parent
145
+ siblings = parent.find_xpath(node.tag_name)
146
+ if siblings.size == 1
147
+ result.unshift node.tag_name
148
+ else
149
+ index = siblings.index(node)
150
+ result.unshift "#{node.tag_name}[#{index+1}]"
151
+ end
152
+ else
153
+ result.unshift node.tag_name
154
+ end
155
+ end
156
+
157
+ '/' + result.join('/')
158
+ end
159
+
124
160
  private
125
161
 
126
162
  # a reference to the select node if this is an option node
@@ -17,7 +17,7 @@ module Capybara
17
17
  else
18
18
  begin
19
19
  @app.call(env)
20
- rescue StandardError => e
20
+ rescue *Capybara.server_errors => e
21
21
  @error = e unless @error
22
22
  raise e
23
23
  end