capybara 2.4.4 → 2.5.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 (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