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.
- checksums.yaml +4 -4
- data/History.md +17 -0
- data/README.md +14 -22
- data/lib/capybara.rb +16 -4
- data/lib/capybara/config.rb +12 -2
- data/lib/capybara/driver/base.rb +4 -0
- data/lib/capybara/node/finders.rb +79 -16
- data/lib/capybara/queries/ancestor_query.rb +25 -0
- data/lib/capybara/queries/selector_query.rb +4 -1
- data/lib/capybara/queries/sibling_query.rb +25 -0
- data/lib/capybara/rack_test/browser.rb +5 -0
- data/lib/capybara/rack_test/driver.rb +5 -1
- data/lib/capybara/rack_test/form.rb +1 -3
- data/lib/capybara/rack_test/node.rb +1 -1
- data/lib/capybara/rspec/compound.rb +95 -0
- data/lib/capybara/rspec/matchers.rb +4 -1
- data/lib/capybara/selector.rb +1 -0
- data/lib/capybara/selector/filter.rb +13 -41
- data/lib/capybara/selector/filter_set.rb +12 -5
- data/lib/capybara/selector/filters/base.rb +33 -0
- data/lib/capybara/selector/filters/expression_filter.rb +40 -0
- data/lib/capybara/selector/filters/node_filter.rb +27 -0
- data/lib/capybara/selector/selector.rb +4 -4
- data/lib/capybara/selenium/driver.rb +12 -2
- data/lib/capybara/selenium/node.rb +70 -55
- data/lib/capybara/session.rb +65 -41
- data/lib/capybara/spec/session/ancestor_spec.rb +85 -0
- data/lib/capybara/spec/session/attach_file_spec.rb +1 -1
- data/lib/capybara/spec/session/check_spec.rb +4 -4
- data/lib/capybara/spec/session/choose_spec.rb +2 -2
- data/lib/capybara/spec/session/click_button_spec.rb +1 -1
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +3 -3
- data/lib/capybara/spec/session/click_link_spec.rb +1 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +2 -2
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/refresh_spec.rb +28 -0
- data/lib/capybara/spec/session/select_spec.rb +2 -2
- data/lib/capybara/spec/session/sibling_spec.rb +52 -0
- data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
- data/lib/capybara/spec/session/unselect_spec.rb +2 -2
- data/lib/capybara/spec/session/window/become_closed_spec.rb +3 -3
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +11 -9
- data/lib/capybara/spec/session/window/within_window_spec.rb +27 -2
- data/lib/capybara/spec/test_app.rb +3 -1
- data/lib/capybara/spec/views/with_html.erb +27 -1
- data/lib/capybara/spec/views/with_windows.erb +4 -0
- data/lib/capybara/version.rb +1 -1
- data/spec/capybara_spec.rb +9 -1
- data/spec/minitest_spec_spec.rb +1 -1
- data/spec/rspec/shared_spec_matchers.rb +146 -42
- data/spec/selenium_spec_chrome.rb +12 -16
- data/spec/selenium_spec_marionette.rb +2 -0
- data/spec/shared_selenium_session.rb +2 -0
- metadata +14 -6
- 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
|
-
|
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
|
|
data/lib/capybara/selector.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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,
|
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
|
-
|
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] =
|
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(:
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
native.
|
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
|
-
|
61
|
+
set_text(value, options)
|
54
62
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
#
|
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
|
-
|
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
|