capybara 2.14.4 → 2.15.0

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