capybara 2.14.4 → 2.15.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +17 -0
  3. data/README.md +14 -22
  4. data/lib/capybara.rb +16 -4
  5. data/lib/capybara/config.rb +12 -2
  6. data/lib/capybara/driver/base.rb +4 -0
  7. data/lib/capybara/node/finders.rb +79 -16
  8. data/lib/capybara/queries/ancestor_query.rb +25 -0
  9. data/lib/capybara/queries/selector_query.rb +4 -1
  10. data/lib/capybara/queries/sibling_query.rb +25 -0
  11. data/lib/capybara/rack_test/browser.rb +5 -0
  12. data/lib/capybara/rack_test/driver.rb +5 -1
  13. data/lib/capybara/rack_test/form.rb +1 -3
  14. data/lib/capybara/rack_test/node.rb +1 -1
  15. data/lib/capybara/rspec/compound.rb +95 -0
  16. data/lib/capybara/rspec/matchers.rb +4 -1
  17. data/lib/capybara/selector.rb +1 -0
  18. data/lib/capybara/selector/filter.rb +13 -41
  19. data/lib/capybara/selector/filter_set.rb +12 -5
  20. data/lib/capybara/selector/filters/base.rb +33 -0
  21. data/lib/capybara/selector/filters/expression_filter.rb +40 -0
  22. data/lib/capybara/selector/filters/node_filter.rb +27 -0
  23. data/lib/capybara/selector/selector.rb +4 -4
  24. data/lib/capybara/selenium/driver.rb +12 -2
  25. data/lib/capybara/selenium/node.rb +70 -55
  26. data/lib/capybara/session.rb +65 -41
  27. data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
  28. data/lib/capybara/spec/session/attach_file_spec.rb +1 -1
  29. data/lib/capybara/spec/session/check_spec.rb +4 -4
  30. data/lib/capybara/spec/session/choose_spec.rb +2 -2
  31. data/lib/capybara/spec/session/click_button_spec.rb +1 -1
  32. data/lib/capybara/spec/session/click_link_or_button_spec.rb +3 -3
  33. data/lib/capybara/spec/session/click_link_spec.rb +1 -1
  34. data/lib/capybara/spec/session/fill_in_spec.rb +2 -2
  35. data/lib/capybara/spec/session/find_spec.rb +1 -1
  36. data/lib/capybara/spec/session/refresh_spec.rb +28 -0
  37. data/lib/capybara/spec/session/select_spec.rb +2 -2
  38. data/lib/capybara/spec/session/sibling_spec.rb +52 -0
  39. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  40. data/lib/capybara/spec/session/unselect_spec.rb +2 -2
  41. data/lib/capybara/spec/session/window/become_closed_spec.rb +3 -3
  42. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +11 -9
  43. data/lib/capybara/spec/session/window/within_window_spec.rb +27 -2
  44. data/lib/capybara/spec/test_app.rb +3 -1
  45. data/lib/capybara/spec/views/with_html.erb +27 -1
  46. data/lib/capybara/spec/views/with_windows.erb +4 -0
  47. data/lib/capybara/version.rb +1 -1
  48. data/spec/capybara_spec.rb +9 -1
  49. data/spec/minitest_spec_spec.rb +1 -1
  50. data/spec/rspec/shared_spec_matchers.rb +146 -42
  51. data/spec/selenium_spec_chrome.rb +12 -16
  52. data/spec/selenium_spec_marionette.rb +2 -0
  53. data/spec/shared_selenium_session.rb +2 -0
  54. metadata +14 -6
  55. data/lib/capybara/selector/expression_filter.rb +0 -40
@@ -2,7 +2,10 @@
2
2
  module Capybara
3
3
  module RSpecMatchers
4
4
  class Matcher
5
- include ::RSpec::Matchers::Composable if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.0'))
5
+ if defined?(::RSpec::Expectations::Version) && (Gem::Version.new(RSpec::Expectations::Version::STRING) >= Gem::Version.new('3.0'))
6
+ require 'capybara/rspec/compound'
7
+ include ::Capybara::RSpecMatchers::Compound
8
+ end
6
9
 
7
10
  attr_reader :failure_message, :failure_message_when_negated
8
11
 
@@ -14,6 +14,7 @@ Capybara::Selector::FilterSet.add(:_field) do
14
14
  states << 'checked' if options[:checked] || (options[:unchecked] == false)
15
15
  states << 'not checked' if options[:unchecked] || (options[:checked] == false)
16
16
  states << 'disabled' if options[:disabled] == true
17
+ states << 'not disabled' if options[:disabled] == false
17
18
  desc << " that is #{states.join(' and ')}" unless states.empty?
18
19
  desc << " with the multiple attribute" if options[:multiple] == true
19
20
  desc << " without the multiple attribute" if options[:multiple] == false
@@ -1,47 +1,19 @@
1
1
  # frozen_string_literal: true
2
+ require 'capybara/selector/filters/node_filter'
3
+ require 'capybara/selector/filters/expression_filter'
4
+
2
5
  module Capybara
3
6
  class Selector
4
- class Filter
5
- def initialize(name, block, options={})
6
- @name = name
7
- @block = block
8
- @options = options
9
- @options[:valid_values] = [true,false] if options[:boolean]
10
- end
11
-
12
- def default?
13
- @options.has_key?(:default)
14
- end
15
-
16
- def default
17
- @options[:default]
18
- end
19
-
20
- def matches?(node, value)
21
- return true if skip?(value)
22
-
23
- if !valid_value?(value)
24
- msg = "Invalid value #{value.inspect} passed to filter #{@name} - "
25
- if default?
26
- warn msg + "defaulting to #{default}"
27
- value = default
28
- else
29
- warn msg + "skipping"
30
- return true
31
- end
32
- end
33
-
34
- @block.call(node, value)
35
- end
36
-
37
- def skip?(value)
38
- @options.has_key?(:skip_if) && value == @options[:skip_if]
39
- end
40
-
41
- private
42
-
43
- def valid_value?(value)
44
- !@options.has_key?(:valid_values) || Array(@options[:valid_values]).include?(value)
7
+ def self.const_missing(const_name)
8
+ case const_name
9
+ when :Filter
10
+ warn "DEPRECATED: Capybara::Selector::Filter is deprecated, please use Capybara::Selector::Filters::NodeFilter instead"
11
+ Filters::NodeFilter
12
+ when :ExpressionFilter
13
+ warn "DEPRECATED: Capybara::Selector::ExpressionFilter is deprecated, please use Capybara::Selector::Filters::ExpressionFilter instead"
14
+ Filters::ExpressionFilter
15
+ else
16
+ super
45
17
  end
46
18
  end
47
19
  end
@@ -13,11 +13,11 @@ module Capybara
13
13
  end
14
14
 
15
15
  def filter(name, *types_and_options, &block)
16
- add_filter(name, Filter, *types_and_options, &block)
16
+ add_filter(name, Filters::NodeFilter, *types_and_options, &block)
17
17
  end
18
18
 
19
19
  def expression_filter(name, *types_and_options, &block)
20
- add_filter(name, ExpressionFilter, *types_and_options, &block)
20
+ add_filter(name, Filters::ExpressionFilter, *types_and_options, &block)
21
21
  end
22
22
 
23
23
  def describe(&block)
@@ -25,7 +25,14 @@ module Capybara
25
25
  end
26
26
 
27
27
  def description(options={})
28
- @descriptions.map {|desc| desc.call(options).to_s }.join
28
+ options_with_defaults = options.dup
29
+ filters.each do |name, filter|
30
+ options_with_defaults[name] = filter.default if filter.default? && !options_with_defaults.has_key?(name)
31
+ end
32
+
33
+ @descriptions.map do |desc|
34
+ desc.call(options_with_defaults).to_s
35
+ end.join
29
36
  end
30
37
 
31
38
  def filters
@@ -33,11 +40,11 @@ module Capybara
33
40
  end
34
41
 
35
42
  def node_filters
36
- filters.reject { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
43
+ filters.reject { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter) }.freeze
37
44
  end
38
45
 
39
46
  def expression_filters
40
- filters.select { |_n, f| f.nil? || f.is_a?(ExpressionFilter) }.freeze
47
+ filters.select { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter) }.freeze
41
48
  end
42
49
 
43
50
  class << self
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ module Capybara
3
+ class Selector
4
+ module Filters
5
+ class Base
6
+ def initialize(name, block, options={})
7
+ @name = name
8
+ @block = block
9
+ @options = options
10
+ @options[:valid_values] = [true,false] if options[:boolean]
11
+ end
12
+
13
+ def default?
14
+ @options.has_key?(:default)
15
+ end
16
+
17
+ def default
18
+ @options[:default]
19
+ end
20
+
21
+ def skip?(value)
22
+ @options.has_key?(:skip_if) && value == @options[:skip_if]
23
+ end
24
+
25
+ private
26
+
27
+ def valid_value?(value)
28
+ !@options.has_key?(:valid_values) || Array(@options[:valid_values]).include?(value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filters/base'
3
+
4
+ module Capybara
5
+ class Selector
6
+ module Filters
7
+ class ExpressionFilter < Base
8
+ def apply_filter(expr, value)
9
+ return expr if skip?(value)
10
+
11
+ if !valid_value?(value)
12
+ msg = "Invalid value #{value.inspect} passed to expression filter #{@name} - "
13
+ if default?
14
+ warn msg + "defaulting to #{default}"
15
+ value = default
16
+ else
17
+ warn msg + "skipping"
18
+ return expr
19
+ end
20
+ end
21
+
22
+ @block.call(expr, value)
23
+ end
24
+ end
25
+
26
+ class IdentityExpressionFilter < ExpressionFilter
27
+ def initialize
28
+ end
29
+
30
+ def default?
31
+ false
32
+ end
33
+
34
+ def apply_filter(expr, _value)
35
+ return expr
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filters/base'
3
+
4
+ module Capybara
5
+ class Selector
6
+ module Filters
7
+ class NodeFilter < Base
8
+ def matches?(node, value)
9
+ return true if skip?(value)
10
+
11
+ if !valid_value?(value)
12
+ msg = "Invalid value #{value.inspect} passed to filter #{@name} - "
13
+ if default?
14
+ warn msg + "defaulting to #{default}"
15
+ value = default
16
+ else
17
+ warn msg + "skipping"
18
+ return true
19
+ end
20
+ end
21
+
22
+ @block.call(node, value)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
- require 'capybara/selector/expression_filter'
3
2
  require 'capybara/selector/filter_set'
4
3
  require 'capybara/selector/css'
5
4
  require 'xpath'
@@ -85,7 +84,7 @@ module Capybara
85
84
  def xpath(*expression_filters, &block)
86
85
  if block
87
86
  @format, @expression = :xpath, block
88
- expression_filters.flatten.each { |ef| custom_filters[ef] = IdentityExpressionFilter.new }
87
+ expression_filters.flatten.each { |ef| custom_filters[ef] = Filters::IdentityExpressionFilter.new }
89
88
  end
90
89
  format == :xpath ? @expression : nil
91
90
  end
@@ -188,13 +187,13 @@ module Capybara
188
187
  def filter(name, *types_and_options, &block)
189
188
  options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
190
189
  types_and_options.each { |k| options[k] = true }
191
- custom_filters[name] = Filter.new(name, block, options)
190
+ custom_filters[name] = Filters::NodeFilter.new(name, block, options)
192
191
  end
193
192
 
194
193
  def expression_filter(name, *types_and_options, &block)
195
194
  options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
196
195
  types_and_options.each { |k| options[k] = true }
197
- custom_filters[name] = ExpressionFilter.new(name, block, options)
196
+ custom_filters[name] = Filters::ExpressionFilter.new(name, block, options)
198
197
  end
199
198
 
200
199
  def filter_set(name, filters_to_use = nil)
@@ -202,6 +201,7 @@ module Capybara
202
201
  f_set.filters.each do |n, filter|
203
202
  custom_filters[n] = filter if filters_to_use.nil? || filters_to_use.include?(n)
204
203
  end
204
+
205
205
  f_set.descriptions.each { |desc| @filter_set.describe(&desc) }
206
206
  end
207
207
 
@@ -67,6 +67,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
67
67
  browser.navigate.to(path)
68
68
  end
69
69
 
70
+ def refresh
71
+ accept_modal(nil, wait: 0.1) do
72
+ browser.navigate.refresh
73
+ end
74
+ rescue Capybara::ModalNotFound
75
+ end
76
+
70
77
  def go_back
71
78
  browser.navigate.back
72
79
  end
@@ -284,7 +291,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
284
291
  ::Selenium::WebDriver::Error::ElementNotVisibleError,
285
292
  ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
286
293
  ::Selenium::WebDriver::Error::ElementNotInteractableError,
287
- ::Selenium::WebDriver::Error::ElementClickInterceptedError]
294
+ ::Selenium::WebDriver::Error::ElementClickInterceptedError,
295
+ ::Selenium::WebDriver::Error::InvalidElementStateError,
296
+ ::Selenium::WebDriver::Error::ElementNotSelectableError,
297
+ ]
288
298
  end
289
299
 
290
300
  def no_such_window_error
@@ -312,7 +322,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
312
322
  caps = @processed_options[:desired_capabilities]
313
323
  chrome_options = caps[:chrome_options] || caps[:chromeOptions] || {}
314
324
  args = chrome_options['args'] || chrome_options[:args] || []
315
- return args.include?("headless")
325
+ return args.include?("--headless") || args.include?("headless")
316
326
  end
317
327
  return false
318
328
  end
@@ -18,7 +18,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
18
18
 
19
19
  def value
20
20
  if tag_name == "select" and multiple?
21
- native.find_elements(:xpath, ".//option").select { |n| n.selected? }.map { |n| n[:value] || n.text }
21
+ native.find_elements(:css, "option:checked").map { |n| n[:value] || n.text }
22
22
  else
23
23
  native[:value]
24
24
  end
@@ -38,65 +38,54 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
38
38
  def set(value, options={})
39
39
  tag_name = self.tag_name
40
40
  type = self[:type]
41
+
41
42
  if (Array === value) && !multiple?
42
43
  raise ArgumentError.new "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}"
43
44
  end
44
- if tag_name == 'input' and type == 'radio'
45
- click
46
- elsif tag_name == 'input' and type == 'checkbox'
47
- click if value ^ native.attribute('checked').to_s.eql?("true")
48
- elsif tag_name == 'input' and type == 'file'
49
- path_names = value.to_s.empty? ? [] : value
50
- if driver.chrome?
51
- native.send_keys(Array(path_names).join("\n"))
45
+
46
+ case tag_name
47
+ when 'input'
48
+ case type
49
+ when 'radio'
50
+ click
51
+ when 'checkbox'
52
+ click if value ^ native.attribute('checked').to_s.eql?("true")
53
+ when 'file'
54
+ path_names = value.to_s.empty? ? [] : value
55
+ if driver.chrome?
56
+ native.send_keys(Array(path_names).join("\n"))
57
+ else
58
+ native.send_keys(*path_names)
59
+ end
52
60
  else
53
- native.send_keys(*path_names)
61
+ set_text(value, options)
54
62
  end
55
- elsif tag_name == 'textarea' or tag_name == 'input'
56
- if readonly?
57
- warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
58
- elsif value.to_s.empty?
59
- native.clear
60
- else
61
- if options[:clear] == :backspace
62
- # Clear field by sending the correct number of backspace keys.
63
- backspaces = [:backspace] * self.value.to_s.length
64
- native.send_keys(*(backspaces + [value.to_s]))
65
- elsif options[:clear] == :none
66
- native.send_keys(value.to_s)
67
- elsif options[:clear].is_a? Array
68
- native.send_keys(*options[:clear], value.to_s)
63
+ when 'textarea'
64
+ set_text(value, options)
65
+ else
66
+ if content_editable?
67
+ #ensure we are focused on the element
68
+ click
69
+
70
+ script = <<-JS
71
+ var range = document.createRange();
72
+ var sel = window.getSelection();
73
+ arguments[0].focus();
74
+ range.selectNodeContents(arguments[0]);
75
+ sel.removeAllRanges();
76
+ sel.addRange(range);
77
+ JS
78
+ driver.execute_script script, self
79
+
80
+ if (driver.chrome?) || (driver.firefox? && !driver.marionette?)
81
+ # chromedriver raises a can't focus element for child elements if we use native.send_keys
82
+ # we've already focused it so just use action api
83
+ driver.browser.action.send_keys(value.to_s).perform
69
84
  else
70
- # Clear field by JavaScript assignment of the value property.
71
- # Script can change a readonly element which user input cannot, so
72
- # don't execute if readonly.
73
- driver.execute_script "arguments[0].value = ''", self
85
+ # action api is really slow here just use native.send_keys
74
86
  native.send_keys(value.to_s)
75
87
  end
76
88
  end
77
- elsif native.attribute('isContentEditable')
78
- #ensure we are focused on the element
79
- native.click
80
-
81
- script = <<-JS
82
- var range = document.createRange();
83
- var sel = window.getSelection();
84
- arguments[0].focus();
85
- range.selectNodeContents(arguments[0]);
86
- sel.removeAllRanges();
87
- sel.addRange(range);
88
- JS
89
- driver.execute_script script, self
90
-
91
- if (driver.chrome?) ||
92
- (driver.firefox? && !driver.marionette?)
93
- # chromedriver raises a can't focus element for child elements if we use native.send_keys
94
- # we've already focused it so just use action api
95
- driver.browser.action.send_keys(value.to_s).perform
96
- else
97
- # action api is really slow here just use native.send_keys
98
- native.send_keys(value.to_s)
99
- end
100
89
  end
101
90
  end
102
91
 
@@ -105,9 +94,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
105
94
  end
106
95
 
107
96
  def unselect_option
108
- if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
109
- raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
110
- end
97
+ raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box." if !select_node.multiple?
111
98
  native.click if selected?
112
99
  end
113
100
 
@@ -173,6 +160,10 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
173
160
  multiple and multiple != "false"
174
161
  end
175
162
 
163
+ def content_editable?
164
+ native.attribute('isContentEditable')
165
+ end
166
+
176
167
  def find_xpath(locator)
177
168
  native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
178
169
  end
@@ -212,6 +203,30 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
212
203
  private
213
204
  # a reference to the select node if this is an option node
214
205
  def select_node
215
- find_xpath('./ancestor::select').first
206
+ find_xpath('./ancestor::select[1]').first
207
+ end
208
+
209
+ def set_text(value, options)
210
+ if readonly?
211
+ warn "Attempt to set readonly element with value: #{value} \n *This will raise an exception in a future version of Capybara"
212
+ elsif value.to_s.empty?
213
+ native.clear
214
+ else
215
+ if options[:clear] == :backspace
216
+ # Clear field by sending the correct number of backspace keys.
217
+ backspaces = [:backspace] * self.value.to_s.length
218
+ native.send_keys(*(backspaces + [value.to_s]))
219
+ elsif options[:clear] == :none
220
+ native.send_keys(value.to_s)
221
+ elsif options[:clear].is_a? Array
222
+ native.send_keys(*options[:clear], value.to_s)
223
+ else
224
+ # Clear field by JavaScript assignment of the value property.
225
+ # Script can change a readonly element which user input cannot, so
226
+ # don't execute if readonly.
227
+ driver.execute_script "arguments[0].value = ''", self
228
+ native.send_keys(value.to_s)
229
+ end
230
+ end
216
231
  end
217
232
  end