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