capybara 2.18.0 → 3.0.0.rc1
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 +26 -1
- data/README.md +12 -12
- data/lib/capybara.rb +13 -25
- data/lib/capybara/config.rb +11 -57
- data/lib/capybara/cucumber.rb +2 -3
- data/lib/capybara/driver/base.rb +5 -16
- data/lib/capybara/driver/node.rb +5 -4
- data/lib/capybara/dsl.rb +1 -0
- data/lib/capybara/helpers.rb +16 -28
- data/lib/capybara/minitest.rb +139 -138
- data/lib/capybara/minitest/spec.rb +15 -14
- data/lib/capybara/node/actions.rb +59 -81
- data/lib/capybara/node/base.rb +11 -18
- data/lib/capybara/node/document.rb +2 -2
- data/lib/capybara/node/document_matchers.rb +8 -8
- data/lib/capybara/node/element.rb +30 -40
- data/lib/capybara/node/finders.rb +62 -70
- data/lib/capybara/node/matchers.rb +48 -71
- data/lib/capybara/node/simple.rb +11 -17
- data/lib/capybara/queries/ancestor_query.rb +4 -6
- data/lib/capybara/queries/base_query.rb +18 -17
- data/lib/capybara/queries/current_path_query.rb +8 -24
- data/lib/capybara/queries/match_query.rb +3 -7
- data/lib/capybara/queries/selector_query.rb +92 -95
- data/lib/capybara/queries/sibling_query.rb +4 -4
- data/lib/capybara/queries/text_query.rb +37 -34
- data/lib/capybara/queries/title_query.rb +8 -11
- data/lib/capybara/rack_test/browser.rb +15 -18
- data/lib/capybara/rack_test/css_handlers.rb +6 -4
- data/lib/capybara/rack_test/driver.rb +6 -10
- data/lib/capybara/rack_test/form.rb +50 -40
- data/lib/capybara/rack_test/node.rb +70 -56
- data/lib/capybara/rails.rb +2 -6
- data/lib/capybara/result.rb +22 -22
- data/lib/capybara/rspec.rb +5 -10
- data/lib/capybara/rspec/compound.rb +5 -10
- data/lib/capybara/rspec/features.rb +17 -48
- data/lib/capybara/rspec/matcher_proxies.rb +31 -15
- data/lib/capybara/rspec/matchers.rb +70 -60
- data/lib/capybara/selector.rb +129 -117
- data/lib/capybara/selector/css.rb +6 -11
- data/lib/capybara/selector/filter.rb +1 -17
- data/lib/capybara/selector/filter_set.rb +17 -14
- data/lib/capybara/selector/filters/base.rb +7 -6
- data/lib/capybara/selector/filters/expression_filter.rb +6 -23
- data/lib/capybara/selector/filters/node_filter.rb +2 -12
- data/lib/capybara/selector/selector.rb +27 -33
- data/lib/capybara/selenium/driver.rb +113 -127
- data/lib/capybara/selenium/node.rb +148 -113
- data/lib/capybara/server.rb +3 -2
- data/lib/capybara/session.rb +137 -223
- data/lib/capybara/session/config.rb +47 -67
- data/lib/capybara/session/matchers.rb +8 -7
- data/lib/capybara/spec/public/test.js +26 -4
- data/lib/capybara/spec/session/accept_alert_spec.rb +1 -0
- data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -2
- data/lib/capybara/spec/session/accept_prompt_spec.rb +1 -0
- data/lib/capybara/spec/session/all_spec.rb +31 -18
- data/lib/capybara/spec/session/ancestor_spec.rb +2 -4
- data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +6 -5
- data/lib/capybara/spec/session/assert_current_path.rb +12 -11
- data/lib/capybara/spec/session/assert_selector.rb +1 -0
- data/lib/capybara/spec/session/assert_text.rb +18 -17
- data/lib/capybara/spec/session/assert_title.rb +1 -0
- data/lib/capybara/spec/session/attach_file_spec.rb +14 -13
- data/lib/capybara/spec/session/body_spec.rb +1 -0
- data/lib/capybara/spec/session/check_spec.rb +7 -6
- data/lib/capybara/spec/session/choose_spec.rb +5 -4
- data/lib/capybara/spec/session/click_button_spec.rb +20 -28
- data/lib/capybara/spec/session/click_link_or_button_spec.rb +8 -7
- data/lib/capybara/spec/session/click_link_spec.rb +8 -7
- data/lib/capybara/spec/session/current_scope_spec.rb +4 -3
- data/lib/capybara/spec/session/current_url_spec.rb +7 -6
- data/lib/capybara/spec/session/dismiss_confirm_spec.rb +1 -1
- data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -0
- data/lib/capybara/spec/session/element/assert_match_selector.rb +1 -1
- data/lib/capybara/spec/session/element/match_xpath_spec.rb +1 -1
- data/lib/capybara/spec/session/element/matches_selector_spec.rb +5 -5
- data/lib/capybara/spec/session/evaluate_async_script_spec.rb +3 -2
- data/lib/capybara/spec/session/evaluate_script_spec.rb +4 -3
- data/lib/capybara/spec/session/execute_script_spec.rb +4 -3
- data/lib/capybara/spec/session/fill_in_spec.rb +6 -5
- data/lib/capybara/spec/session/find_button_spec.rb +4 -3
- data/lib/capybara/spec/session/find_by_id_spec.rb +2 -1
- data/lib/capybara/spec/session/find_field_spec.rb +8 -14
- data/lib/capybara/spec/session/find_link_spec.rb +6 -5
- data/lib/capybara/spec/session/find_spec.rb +37 -31
- data/lib/capybara/spec/session/first_spec.rb +60 -33
- data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +2 -1
- data/lib/capybara/spec/session/frame/within_frame_spec.rb +9 -16
- data/lib/capybara/spec/session/go_back_spec.rb +1 -0
- data/lib/capybara/spec/session/go_forward_spec.rb +1 -0
- data/lib/capybara/spec/session/has_all_selectors_spec.rb +15 -15
- data/lib/capybara/spec/session/has_button_spec.rb +2 -1
- data/lib/capybara/spec/session/has_css_spec.rb +3 -2
- data/lib/capybara/spec/session/has_current_path_spec.rb +12 -28
- data/lib/capybara/spec/session/has_field_spec.rb +4 -3
- data/lib/capybara/spec/session/has_link_spec.rb +1 -0
- data/lib/capybara/spec/session/has_none_selectors_spec.rb +17 -17
- data/lib/capybara/spec/session/has_select_spec.rb +30 -29
- data/lib/capybara/spec/session/has_selector_spec.rb +5 -4
- data/lib/capybara/spec/session/has_table_spec.rb +2 -1
- data/lib/capybara/spec/session/has_text_spec.rb +6 -5
- data/lib/capybara/spec/session/has_title_spec.rb +1 -0
- data/lib/capybara/spec/session/has_xpath_spec.rb +1 -0
- data/lib/capybara/spec/session/headers.rb +2 -1
- data/lib/capybara/spec/session/html_spec.rb +1 -0
- data/lib/capybara/spec/session/node_spec.rb +91 -56
- data/lib/capybara/spec/session/node_wrapper_spec.rb +36 -0
- data/lib/capybara/spec/session/refresh_spec.rb +4 -2
- data/lib/capybara/spec/session/reset_session_spec.rb +1 -0
- data/lib/capybara/spec/session/response_code.rb +1 -0
- data/lib/capybara/spec/session/save_and_open_page_spec.rb +1 -0
- data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +6 -11
- data/lib/capybara/spec/session/save_page_spec.rb +1 -17
- data/lib/capybara/spec/session/save_screenshot_spec.rb +1 -1
- data/lib/capybara/spec/session/select_spec.rb +20 -20
- data/lib/capybara/spec/session/selectors_spec.rb +2 -2
- data/lib/capybara/spec/session/sibling_spec.rb +1 -1
- data/lib/capybara/spec/session/text_spec.rb +1 -0
- data/lib/capybara/spec/session/title_spec.rb +1 -1
- data/lib/capybara/spec/session/uncheck_spec.rb +4 -3
- data/lib/capybara/spec/session/unselect_spec.rb +6 -5
- data/lib/capybara/spec/session/visit_spec.rb +9 -3
- data/lib/capybara/spec/session/window/become_closed_spec.rb +2 -1
- data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
- data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
- data/lib/capybara/spec/session/window/switch_to_window_spec.rb +2 -1
- data/lib/capybara/spec/session/window/window_opened_by_spec.rb +2 -1
- data/lib/capybara/spec/session/window/window_spec.rb +12 -12
- data/lib/capybara/spec/session/window/windows_spec.rb +2 -3
- data/lib/capybara/spec/session/window/within_window_spec.rb +13 -68
- data/lib/capybara/spec/session/within_spec.rb +1 -0
- data/lib/capybara/spec/spec_helper.rb +26 -18
- data/lib/capybara/spec/test_app.rb +8 -9
- data/lib/capybara/spec/views/form.erb +1 -0
- data/lib/capybara/spec/views/with_html.erb +3 -1
- data/lib/capybara/spec/views/within_frames.erb +4 -1
- data/lib/capybara/version.rb +2 -1
- data/lib/capybara/window.rb +6 -10
- data/spec/basic_node_spec.rb +1 -0
- data/spec/capybara_spec.rb +9 -32
- data/spec/dsl_spec.rb +5 -13
- data/spec/filter_set_spec.rb +5 -4
- data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -1
- data/spec/fixtures/selenium_driver_rspec_success.rb +3 -2
- data/spec/minitest_spec.rb +4 -3
- data/spec/minitest_spec_spec.rb +3 -2
- data/spec/per_session_config_spec.rb +9 -8
- data/spec/rack_test_spec.rb +21 -20
- data/spec/result_spec.rb +17 -16
- data/spec/rspec/features_spec.rb +17 -14
- data/spec/rspec/scenarios_spec.rb +5 -7
- data/spec/rspec/shared_spec_matchers.rb +96 -99
- data/spec/rspec/views_spec.rb +2 -1
- data/spec/rspec_matchers_spec.rb +19 -2
- data/spec/rspec_spec.rb +11 -15
- data/spec/selector_spec.rb +5 -6
- data/spec/selenium_spec_chrome.rb +7 -4
- data/spec/selenium_spec_marionette.rb +26 -12
- data/spec/server_spec.rb +33 -33
- data/spec/session_spec.rb +2 -1
- data/spec/shared_selenium_session.rb +27 -21
- data/spec/spec_helper.rb +2 -5
- metadata +66 -87
- data/lib/capybara/query.rb +0 -7
- data/spec/selenium_spec_firefox.rb +0 -68
@@ -2,24 +2,19 @@ module Capybara
|
|
2
2
|
class Selector
|
3
3
|
class CSS
|
4
4
|
def self.escape(str)
|
5
|
-
out = String.new("")
|
6
5
|
value = str.dup
|
6
|
+
out = "".dup
|
7
7
|
out << value.slice!(0...1) if value =~ /^[-_]/
|
8
|
-
out <<
|
9
|
-
|
10
|
-
else
|
11
|
-
escape_char(value.slice!(0...1))
|
12
|
-
end
|
13
|
-
out << value.gsub(/[^a-zA-Z0-9_-]/) {|c| escape_char c}
|
8
|
+
out << (value[0] =~ NMSTART ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
|
9
|
+
out << value.gsub(/[^a-zA-Z0-9_-]/) { |c| escape_char c }
|
14
10
|
out
|
15
11
|
end
|
16
12
|
|
17
13
|
def self.escape_char(c)
|
18
|
-
|
19
|
-
"\\#{c}"
|
14
|
+
c =~ %r{[ -/:-~]} ? "\\#{c}" : format("\\%06x", c.ord)
|
20
15
|
end
|
21
16
|
|
22
|
-
S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
|
17
|
+
S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'.freeze
|
23
18
|
H = /[0-9a-fA-F]/
|
24
19
|
UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
|
25
20
|
NONASCII = /[#{S}]/
|
@@ -27,4 +22,4 @@ module Capybara
|
|
27
22
|
NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
|
28
23
|
end
|
29
24
|
end
|
30
|
-
end
|
25
|
+
end
|
@@ -1,20 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'capybara/selector/filters/node_filter'
|
3
4
|
require 'capybara/selector/filters/expression_filter'
|
4
|
-
|
5
|
-
module Capybara
|
6
|
-
class Selector
|
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
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'capybara/selector/filter'
|
3
4
|
|
4
5
|
module Capybara
|
@@ -24,14 +25,10 @@ module Capybara
|
|
24
25
|
descriptions.push block
|
25
26
|
end
|
26
27
|
|
27
|
-
def description(options
|
28
|
-
|
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
|
-
|
28
|
+
def description(**options)
|
29
|
+
opts = options_with_defaults(options)
|
33
30
|
@descriptions.map do |desc|
|
34
|
-
desc.call(
|
31
|
+
desc.call(opts).to_s
|
35
32
|
end.join
|
36
33
|
end
|
37
34
|
|
@@ -44,11 +41,10 @@ module Capybara
|
|
44
41
|
end
|
45
42
|
|
46
43
|
def expression_filters
|
47
|
-
filters.select { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter)
|
44
|
+
filters.select { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter) }.freeze
|
48
45
|
end
|
49
46
|
|
50
47
|
class << self
|
51
|
-
|
52
48
|
def all
|
53
49
|
@filter_sets ||= {}
|
54
50
|
end
|
@@ -62,13 +58,20 @@ module Capybara
|
|
62
58
|
end
|
63
59
|
end
|
64
60
|
|
65
|
-
|
61
|
+
private
|
62
|
+
|
63
|
+
def options_with_defaults(options)
|
64
|
+
options = options.dup
|
65
|
+
filters.each do |name, filter|
|
66
|
+
options[name] = filter.default if filter.default? && !options.key?(name)
|
67
|
+
end
|
68
|
+
options
|
69
|
+
end
|
66
70
|
|
67
|
-
def add_filter(name, filter_class, *
|
68
|
-
|
69
|
-
types_and_options.each { |k| options[k] = true}
|
71
|
+
def add_filter(name, filter_class, *types, **options, &block)
|
72
|
+
types.each { |k| options[k] = true }
|
70
73
|
filters[name] = filter_class.new(name, block, options)
|
71
74
|
end
|
72
75
|
end
|
73
76
|
end
|
74
|
-
end
|
77
|
+
end
|
@@ -1,17 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Capybara
|
3
4
|
class Selector
|
4
5
|
module Filters
|
5
6
|
class Base
|
6
|
-
def initialize(name, block, options
|
7
|
+
def initialize(name, block, **options)
|
7
8
|
@name = name
|
8
9
|
@block = block
|
9
10
|
@options = options
|
10
|
-
@options[:valid_values] = [true,false] if options[:boolean]
|
11
|
+
@options[:valid_values] = [true, false] if options[:boolean]
|
11
12
|
end
|
12
13
|
|
13
14
|
def default?
|
14
|
-
@options.
|
15
|
+
@options.key?(:default)
|
15
16
|
end
|
16
17
|
|
17
18
|
def default
|
@@ -19,13 +20,13 @@ module Capybara
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def skip?(value)
|
22
|
-
@options.
|
23
|
+
@options.key?(:skip_if) && value == @options[:skip_if]
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
+
private
|
26
27
|
|
27
28
|
def valid_value?(value)
|
28
|
-
!@options.
|
29
|
+
!@options.key?(:valid_values) || Array(@options[:valid_values]).include?(value)
|
29
30
|
end
|
30
31
|
end
|
31
32
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'capybara/selector/filters/base'
|
3
4
|
|
4
5
|
module Capybara
|
@@ -7,34 +8,16 @@ module Capybara
|
|
7
8
|
class ExpressionFilter < Base
|
8
9
|
def apply_filter(expr, value)
|
9
10
|
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
|
-
|
11
|
+
raise "ArgumentError", "Invalid value #{value.inspect} passed to expression filter #{@name}" unless valid_value?(value)
|
22
12
|
@block.call(expr, value)
|
23
13
|
end
|
24
14
|
end
|
25
15
|
|
26
16
|
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
|
17
|
+
def initialize; end
|
18
|
+
def default?; false; end
|
19
|
+
def apply_filter(expr, _value); expr; end
|
37
20
|
end
|
38
21
|
end
|
39
22
|
end
|
40
|
-
end
|
23
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'capybara/selector/filters/base'
|
3
4
|
|
4
5
|
module Capybara
|
@@ -7,18 +8,7 @@ module Capybara
|
|
7
8
|
class NodeFilter < Base
|
8
9
|
def matches?(node, value)
|
9
10
|
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
|
-
|
11
|
+
raise ArgumentError, "Invalid value #{value.inspect} passed to filter #{@name}" unless valid_value?(value)
|
22
12
|
@block.call(node, value)
|
23
13
|
end
|
24
14
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'capybara/selector/filter_set'
|
3
4
|
require 'capybara/selector/css'
|
4
5
|
require 'xpath'
|
5
6
|
|
6
|
-
#Patch XPath to allow a nil condition in where
|
7
|
+
# Patch XPath to allow a nil condition in where
|
7
8
|
module XPath
|
8
9
|
class Renderer
|
9
10
|
undef :where if method_defined?(:where)
|
@@ -12,7 +13,7 @@ module XPath
|
|
12
13
|
if !condition.empty?
|
13
14
|
"#{on}[#{condition}]"
|
14
15
|
else
|
15
|
-
|
16
|
+
on.to_s
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -20,7 +21,6 @@ end
|
|
20
21
|
|
21
22
|
module Capybara
|
22
23
|
class Selector
|
23
|
-
|
24
24
|
attr_reader :name, :format
|
25
25
|
|
26
26
|
class << self
|
@@ -43,7 +43,7 @@ module Capybara
|
|
43
43
|
|
44
44
|
def initialize(name, &block)
|
45
45
|
@name = name
|
46
|
-
@filter_set = FilterSet.add(name){}
|
46
|
+
@filter_set = FilterSet.add(name) {}
|
47
47
|
@match = nil
|
48
48
|
@label = nil
|
49
49
|
@failure_message = nil
|
@@ -134,7 +134,7 @@ module Capybara
|
|
134
134
|
# @overload label()
|
135
135
|
# @return [String] The currently set label
|
136
136
|
#
|
137
|
-
def label(label=nil)
|
137
|
+
def label(label = nil)
|
138
138
|
@label = label if label
|
139
139
|
@label
|
140
140
|
end
|
@@ -146,13 +146,12 @@ module Capybara
|
|
146
146
|
# @param [Hash] options The options of the query used to generate the description
|
147
147
|
# @return [String] Description of the selector when used with the options passed
|
148
148
|
#
|
149
|
-
def description(options
|
149
|
+
def description(**options)
|
150
150
|
@filter_set.description(options)
|
151
151
|
end
|
152
152
|
|
153
|
-
def call(locator, options
|
153
|
+
def call(locator, **options)
|
154
154
|
if format
|
155
|
-
# @expression.call(locator, options.select {|k,v| @expression_filters.include?(k)})
|
156
155
|
@expression.call(locator, options)
|
157
156
|
else
|
158
157
|
warn "Selector has no format"
|
@@ -185,15 +184,11 @@ module Capybara
|
|
185
184
|
# @option options :skip_if Value of the filter that will cause it to be skipped
|
186
185
|
#
|
187
186
|
def filter(name, *types_and_options, &block)
|
188
|
-
|
189
|
-
types_and_options.each { |k| options[k] = true }
|
190
|
-
custom_filters[name] = Filters::NodeFilter.new(name, block, options)
|
187
|
+
add_filter(name, Filters::NodeFilter, *types_and_options, &block)
|
191
188
|
end
|
192
189
|
|
193
190
|
def expression_filter(name, *types_and_options, &block)
|
194
|
-
|
195
|
-
types_and_options.each { |k| options[k] = true }
|
196
|
-
custom_filters[name] = Filters::ExpressionFilter.new(name, block, options)
|
191
|
+
add_filter(name, Filters::ExpressionFilter, *types_and_options, &block)
|
197
192
|
end
|
198
193
|
|
199
194
|
def filter_set(name, filters_to_use = nil)
|
@@ -205,7 +200,7 @@ module Capybara
|
|
205
200
|
f_set.descriptions.each { |desc| @filter_set.describe(&desc) }
|
206
201
|
end
|
207
202
|
|
208
|
-
def describe
|
203
|
+
def describe(&block)
|
209
204
|
@filter_set.describe(&block)
|
210
205
|
end
|
211
206
|
|
@@ -230,17 +225,22 @@ module Capybara
|
|
230
225
|
end
|
231
226
|
end
|
232
227
|
|
233
|
-
|
228
|
+
private
|
229
|
+
|
230
|
+
def add_filter(name, filter_class, *types, **options, &block)
|
231
|
+
types.each { |k| options[k] = true }
|
232
|
+
custom_filters[name] = filter_class.new(name, block, options)
|
233
|
+
end
|
234
234
|
|
235
|
-
def locate_field(xpath, locator,
|
236
|
-
locate_xpath = xpath #
|
235
|
+
def locate_field(xpath, locator, enable_aria_label: false, **_options)
|
236
|
+
locate_xpath = xpath # Need to save original xpath for the label wrap
|
237
237
|
if locator
|
238
238
|
locator = locator.to_s
|
239
|
-
attr_matchers =
|
240
|
-
XPath.attr(:name)
|
241
|
-
XPath.attr(:placeholder)
|
242
|
-
XPath.attr(:id)
|
243
|
-
attr_matchers
|
239
|
+
attr_matchers = [XPath.attr(:id) == locator,
|
240
|
+
XPath.attr(:name) == locator,
|
241
|
+
XPath.attr(:placeholder) == locator,
|
242
|
+
XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)].reduce(:|)
|
243
|
+
attr_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
|
244
244
|
|
245
245
|
locate_xpath = locate_xpath[attr_matchers]
|
246
246
|
locate_xpath = locate_xpath.union(XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath))
|
@@ -250,8 +250,8 @@ module Capybara
|
|
250
250
|
locate_xpath
|
251
251
|
end
|
252
252
|
|
253
|
-
def describe_all_expression_filters(opts
|
254
|
-
expression_filters.map { |ef| " with #{ef} #{opts[ef]}" if opts.
|
253
|
+
def describe_all_expression_filters(**opts)
|
254
|
+
expression_filters.map { |ef| " with #{ef} #{opts[ef]}" if opts.key?(ef) }.join
|
255
255
|
end
|
256
256
|
|
257
257
|
def find_by_attr(attribute, value)
|
@@ -259,18 +259,12 @@ module Capybara
|
|
259
259
|
if respond_to?(finder_name, true)
|
260
260
|
send(finder_name, value)
|
261
261
|
else
|
262
|
-
value ? XPath.attr(attribute)
|
262
|
+
value ? XPath.attr(attribute) == value : nil
|
263
263
|
end
|
264
264
|
end
|
265
265
|
|
266
266
|
def find_by_class_attr(classes)
|
267
|
-
|
268
|
-
Array(classes).map do |klass|
|
269
|
-
"contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
|
270
|
-
end.join(" and ").to_sym
|
271
|
-
else
|
272
|
-
nil
|
273
|
-
end
|
267
|
+
Array(classes).map { |klass| XPath.attr(:class).contains_word(klass) }.reduce(:&)
|
274
268
|
end
|
275
269
|
end
|
276
270
|
end
|
@@ -1,14 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "uri"
|
4
|
+
require "English"
|
3
5
|
|
4
6
|
class Capybara::Selenium::Driver < Capybara::Driver::Base
|
5
|
-
|
6
7
|
DEFAULT_OPTIONS = {
|
7
|
-
:
|
8
|
+
browser: :firefox,
|
8
9
|
clear_local_storage: false,
|
9
10
|
clear_session_storage: false
|
10
|
-
}
|
11
|
-
SPECIAL_OPTIONS = [
|
11
|
+
}.freeze
|
12
|
+
SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage].freeze
|
12
13
|
|
13
14
|
attr_reader :app, :options
|
14
15
|
|
@@ -16,10 +17,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
16
17
|
unless @browser
|
17
18
|
if firefox?
|
18
19
|
options[:desired_capabilities] ||= {}
|
19
|
-
options[:desired_capabilities]
|
20
|
+
options[:desired_capabilities][:unexpectedAlertBehaviour] = "ignore"
|
20
21
|
end
|
21
22
|
|
22
|
-
@processed_options = options.reject { |key,_val| SPECIAL_OPTIONS.include?(key) }
|
23
|
+
@processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
|
23
24
|
@browser = Selenium::WebDriver.for(options[:browser], @processed_options)
|
24
25
|
|
25
26
|
@w3c = ((defined?(Selenium::WebDriver::Remote::W3CCapabilities) && @browser.capabilities.is_a?(Selenium::WebDriver::Remote::W3CCapabilities)) ||
|
@@ -27,7 +28,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
27
28
|
main = Process.pid
|
28
29
|
at_exit do
|
29
30
|
# Store the exit status of the test run since it goes away after calling the at_exit proc...
|
30
|
-
@exit_status =
|
31
|
+
@exit_status = $ERROR_INFO.status if $ERROR_INFO.is_a?(SystemExit)
|
31
32
|
quit if Process.pid == main
|
32
33
|
exit @exit_status if @exit_status # Force exit with stored status
|
33
34
|
end
|
@@ -35,7 +36,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
35
36
|
@browser
|
36
37
|
end
|
37
38
|
|
38
|
-
def initialize(app, options
|
39
|
+
def initialize(app, **options)
|
39
40
|
load_selenium
|
40
41
|
@session = nil
|
41
42
|
@app = app
|
@@ -50,10 +51,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
50
51
|
end
|
51
52
|
|
52
53
|
def refresh
|
53
|
-
|
54
|
-
browser.navigate.refresh
|
55
|
-
end
|
56
|
-
rescue Capybara::ModalNotFound
|
54
|
+
browser.navigate.refresh
|
57
55
|
end
|
58
56
|
|
59
57
|
def go_back
|
@@ -88,7 +86,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
88
86
|
def needs_server?; true; end
|
89
87
|
|
90
88
|
def execute_script(script, *args)
|
91
|
-
browser.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ?
|
89
|
+
browser.execute_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg })
|
92
90
|
end
|
93
91
|
|
94
92
|
def evaluate_script(script, *args)
|
@@ -98,66 +96,64 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
98
96
|
|
99
97
|
def evaluate_async_script(script, *args)
|
100
98
|
browser.manage.timeouts.script_timeout = Capybara.default_max_wait_time
|
101
|
-
result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg}
|
99
|
+
result = browser.execute_async_script(script, *args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg })
|
102
100
|
unwrap_script_result(result)
|
103
101
|
end
|
104
102
|
|
105
|
-
def save_screenshot(path, _options
|
103
|
+
def save_screenshot(path, **_options)
|
106
104
|
browser.save_screenshot(path)
|
107
105
|
end
|
108
106
|
|
109
107
|
def reset!
|
110
108
|
# Use instance variable directly so we avoid starting the browser just to reset the session
|
111
|
-
|
112
|
-
|
113
|
-
|
109
|
+
return unless @browser
|
110
|
+
|
111
|
+
navigated = false
|
112
|
+
start_time = Capybara::Helpers.monotonic_time
|
113
|
+
begin
|
114
|
+
unless navigated
|
115
|
+
# Only trigger a navigation if we haven't done it already, otherwise it
|
116
|
+
# can trigger an endless series of unload modals
|
117
|
+
begin
|
118
|
+
@browser.manage.delete_all_cookies
|
119
|
+
clear_storage
|
120
|
+
rescue Selenium::WebDriver::Error::UnhandledError
|
121
|
+
# delete_all_cookies fails when we've previously gone
|
122
|
+
# to about:blank, so we rescue this error and do nothing
|
123
|
+
# instead.
|
124
|
+
end
|
125
|
+
@browser.navigate.to("about:blank")
|
126
|
+
end
|
127
|
+
navigated = true
|
128
|
+
|
129
|
+
# Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
|
130
|
+
until find_xpath("/html/body/*").empty?
|
131
|
+
raise Capybara::ExpectationNotMet, 'Timed out waiting for Selenium session reset' if (Capybara::Helpers.monotonic_time - start_time) >= 10
|
132
|
+
sleep 0.05
|
133
|
+
end
|
134
|
+
rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
135
|
+
# This error is thrown if an unhandled alert is on the page
|
136
|
+
# Firefox appears to automatically dismiss this alert, chrome does not
|
137
|
+
# We'll try to accept it
|
114
138
|
begin
|
115
|
-
|
116
|
-
|
117
|
-
|
139
|
+
@browser.switch_to.alert.accept
|
140
|
+
sleep 0.25 # allow time for the modal to be handled
|
141
|
+
rescue modal_error
|
142
|
+
# The alert is now gone
|
143
|
+
if current_url != "about:blank"
|
118
144
|
begin
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
end
|
127
|
-
if options[:clear_local_storage]
|
128
|
-
if @browser.respond_to? :local_storage
|
129
|
-
@browser.local_storage.clear
|
130
|
-
else
|
131
|
-
warn "localStorage clear requested but is not available for this driver"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
rescue Selenium::WebDriver::Error::UnhandledError
|
135
|
-
# delete_all_cookies fails when we've previously gone
|
136
|
-
# to about:blank, so we rescue this error and do nothing
|
137
|
-
# instead.
|
145
|
+
# If navigation has not occurred attempt again and accept alert
|
146
|
+
# since FF may have dismissed the alert at first attempt
|
147
|
+
@browser.navigate.to("about:blank")
|
148
|
+
sleep 0.1 # slight wait for alert
|
149
|
+
@browser.switch_to.alert.accept
|
150
|
+
rescue modal_error # rubocop:disable Metrics/BlockNesting
|
151
|
+
# alert now gone, should mean navigation happened
|
138
152
|
end
|
139
|
-
@browser.navigate.to("about:blank")
|
140
153
|
end
|
141
|
-
navigated = true
|
142
|
-
|
143
|
-
#Ensure the page is empty and trigger an UnhandledAlertError for any modals that appear during unload
|
144
|
-
until find_xpath("/html/body/*").empty? do
|
145
|
-
raise Capybara::ExpectationNotMet.new('Timed out waiting for Selenium session reset') if (Capybara::Helpers.monotonic_time - start_time) >= 10
|
146
|
-
sleep 0.05
|
147
|
-
end
|
148
|
-
rescue Selenium::WebDriver::Error::UnhandledAlertError, Selenium::WebDriver::Error::UnexpectedAlertOpenError
|
149
|
-
# This error is thrown if an unhandled alert is on the page
|
150
|
-
# Firefox appears to automatically dismiss this alert, chrome does not
|
151
|
-
# We'll try to accept it
|
152
|
-
begin
|
153
|
-
@browser.switch_to.alert.accept
|
154
|
-
sleep 0.25 # allow time for the modal to be handled
|
155
|
-
rescue modal_error
|
156
|
-
# The alert is now gone - nothing to do
|
157
|
-
end
|
158
|
-
# try cleaning up the browser again
|
159
|
-
retry
|
160
154
|
end
|
155
|
+
# try cleaning up the browser again
|
156
|
+
retry
|
161
157
|
end
|
162
158
|
end
|
163
159
|
|
@@ -226,12 +222,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
226
222
|
browser.switch_to.window handle
|
227
223
|
end
|
228
224
|
|
229
|
-
def
|
230
|
-
handle = find_window(locator)
|
231
|
-
browser.switch_to.window(handle) { yield }
|
232
|
-
end
|
233
|
-
|
234
|
-
def accept_modal(_type, options={})
|
225
|
+
def accept_modal(_type, **options)
|
235
226
|
yield if block_given?
|
236
227
|
modal = find_modal(options)
|
237
228
|
|
@@ -242,7 +233,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
242
233
|
message
|
243
234
|
end
|
244
235
|
|
245
|
-
def dismiss_modal(_type, options
|
236
|
+
def dismiss_modal(_type, **options)
|
246
237
|
yield if block_given?
|
247
238
|
modal = find_modal(options)
|
248
239
|
message = modal.text
|
@@ -264,14 +255,15 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
264
255
|
end
|
265
256
|
|
266
257
|
def invalid_element_errors
|
267
|
-
[
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
258
|
+
[
|
259
|
+
::Selenium::WebDriver::Error::StaleElementReferenceError,
|
260
|
+
::Selenium::WebDriver::Error::UnhandledError,
|
261
|
+
::Selenium::WebDriver::Error::ElementNotVisibleError,
|
262
|
+
::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
|
263
|
+
::Selenium::WebDriver::Error::ElementNotInteractableError,
|
264
|
+
::Selenium::WebDriver::Error::ElementClickInterceptedError,
|
265
|
+
::Selenium::WebDriver::Error::InvalidElementStateError,
|
266
|
+
::Selenium::WebDriver::Error::ElementNotSelectableError
|
275
267
|
]
|
276
268
|
end
|
277
269
|
|
@@ -294,18 +286,29 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
294
286
|
browser_name == "chrome"
|
295
287
|
end
|
296
288
|
|
297
|
-
|
298
|
-
def browser_initialized?
|
299
|
-
super && !@browser.nil?
|
300
|
-
end
|
301
|
-
|
302
|
-
private
|
289
|
+
private
|
303
290
|
|
304
|
-
# @api private
|
305
291
|
def browser_name
|
306
292
|
options[:browser].to_s
|
307
293
|
end
|
308
294
|
|
295
|
+
def clear_storage
|
296
|
+
if options[:clear_session_storage]
|
297
|
+
if @browser.respond_to? :session_storage
|
298
|
+
@browser.session_storage.clear
|
299
|
+
else
|
300
|
+
warn "sessionStorage clear requested but is not available for this driver"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
if options[:clear_local_storage]
|
304
|
+
if @browser.respond_to? :local_storage
|
305
|
+
@browser.local_storage.clear
|
306
|
+
else
|
307
|
+
warn "localStorage clear requested but is not available for this driver"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
309
312
|
def modal_error
|
310
313
|
if defined?(Selenium::WebDriver::Error::NoSuchAlertError)
|
311
314
|
Selenium::WebDriver::Error::NoSuchAlertError
|
@@ -314,23 +317,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
314
317
|
end
|
315
318
|
end
|
316
319
|
|
317
|
-
def find_window(locator)
|
318
|
-
handles = browser.window_handles
|
319
|
-
return locator if handles.include? locator
|
320
|
-
|
321
|
-
original_handle = browser.window_handle
|
322
|
-
handles.each do |handle|
|
323
|
-
switch_to_window(handle)
|
324
|
-
if (locator == browser.execute_script("return window.name") ||
|
325
|
-
browser.title.include?(locator) ||
|
326
|
-
browser.current_url.include?(locator))
|
327
|
-
switch_to_window(original_handle)
|
328
|
-
return handle
|
329
|
-
end
|
330
|
-
end
|
331
|
-
raise Capybara::ElementNotFound, "Could not find a window identified by #{locator}"
|
332
|
-
end
|
333
|
-
|
334
320
|
def insert_modal_handlers(accept, response_text)
|
335
321
|
prompt_response = if accept
|
336
322
|
if response_text.nil?
|
@@ -386,7 +372,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
386
372
|
end
|
387
373
|
|
388
374
|
def within_given_window(handle)
|
389
|
-
original_handle =
|
375
|
+
original_handle = current_window_handle
|
390
376
|
if handle == original_handle
|
391
377
|
yield
|
392
378
|
else
|
@@ -397,39 +383,41 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
397
383
|
end
|
398
384
|
end
|
399
385
|
|
400
|
-
def find_modal(options
|
386
|
+
def find_modal(text: nil, **options)
|
401
387
|
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
|
402
388
|
# Actual wait time may be longer than specified
|
403
389
|
wait = Selenium::WebDriver::Wait.new(
|
404
|
-
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0
|
405
|
-
ignore: modal_error
|
390
|
+
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0,
|
391
|
+
ignore: modal_error
|
392
|
+
)
|
406
393
|
begin
|
407
394
|
wait.until do
|
408
395
|
alert = @browser.switch_to.alert
|
409
|
-
regexp =
|
396
|
+
regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
|
410
397
|
alert.text.match(regexp) ? alert : nil
|
411
398
|
end
|
412
399
|
rescue Selenium::WebDriver::Error::TimeOutError
|
413
|
-
raise Capybara::ModalNotFound
|
400
|
+
raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
|
414
401
|
end
|
415
402
|
end
|
416
403
|
|
417
|
-
def find_headless_modal(options
|
404
|
+
def find_headless_modal(text: nil, **options)
|
418
405
|
# Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time
|
419
406
|
# Actual wait time may be longer than specified
|
420
407
|
wait = Selenium::WebDriver::Wait.new(
|
421
|
-
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0
|
422
|
-
ignore: modal_error
|
408
|
+
timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0,
|
409
|
+
ignore: modal_error
|
410
|
+
)
|
423
411
|
begin
|
424
412
|
wait.until do
|
425
413
|
called, alert_text = evaluate_script('window.capybara && window.capybara.current_modal_status()')
|
426
414
|
if called
|
427
415
|
execute_script('window.capybara && window.capybara.modal_handlers.shift()')
|
428
|
-
regexp =
|
416
|
+
regexp = text.is_a?(Regexp) ? text : Regexp.escape(text.to_s)
|
429
417
|
if alert_text.match(regexp)
|
430
418
|
alert_text
|
431
419
|
else
|
432
|
-
raise Capybara::ModalNotFound
|
420
|
+
raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{text}" if text}"
|
433
421
|
end
|
434
422
|
elsif called.nil?
|
435
423
|
# page changed so modal_handler data has gone away
|
@@ -440,7 +428,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
440
428
|
end
|
441
429
|
end
|
442
430
|
rescue Selenium::WebDriver::Error::TimeOutError
|
443
|
-
raise Capybara::ModalNotFound
|
431
|
+
raise Capybara::ModalNotFound, "Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}"
|
444
432
|
end
|
445
433
|
end
|
446
434
|
|
@@ -449,7 +437,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
449
437
|
end
|
450
438
|
|
451
439
|
def silenced_unknown_error_messages
|
452
|
-
[
|
440
|
+
[/Error communicating with the remote browser/]
|
453
441
|
end
|
454
442
|
|
455
443
|
def unwrap_script_result(arg)
|
@@ -466,21 +454,19 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
|
|
466
454
|
end
|
467
455
|
|
468
456
|
def load_selenium
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
raise e
|
483
|
-
end
|
457
|
+
require 'selenium-webdriver'
|
458
|
+
# Fix for selenium-webdriver 3.4.0 which misnamed these
|
459
|
+
unless defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
|
460
|
+
::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
|
461
|
+
end
|
462
|
+
unless defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
|
463
|
+
::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
|
464
|
+
end
|
465
|
+
rescue LoadError => e
|
466
|
+
if e.message =~ /selenium-webdriver/
|
467
|
+
raise LoadError, "Capybara's selenium driver is unable to load `selenium-webdriver`, please install the gem and add `gem 'selenium-webdriver'` to your Gemfile if you are using bundler."
|
468
|
+
else
|
469
|
+
raise e
|
484
470
|
end
|
485
471
|
end
|
486
472
|
end
|