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