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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +26 -1
  3. data/README.md +12 -12
  4. data/lib/capybara.rb +13 -25
  5. data/lib/capybara/config.rb +11 -57
  6. data/lib/capybara/cucumber.rb +2 -3
  7. data/lib/capybara/driver/base.rb +5 -16
  8. data/lib/capybara/driver/node.rb +5 -4
  9. data/lib/capybara/dsl.rb +1 -0
  10. data/lib/capybara/helpers.rb +16 -28
  11. data/lib/capybara/minitest.rb +139 -138
  12. data/lib/capybara/minitest/spec.rb +15 -14
  13. data/lib/capybara/node/actions.rb +59 -81
  14. data/lib/capybara/node/base.rb +11 -18
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +8 -8
  17. data/lib/capybara/node/element.rb +30 -40
  18. data/lib/capybara/node/finders.rb +62 -70
  19. data/lib/capybara/node/matchers.rb +48 -71
  20. data/lib/capybara/node/simple.rb +11 -17
  21. data/lib/capybara/queries/ancestor_query.rb +4 -6
  22. data/lib/capybara/queries/base_query.rb +18 -17
  23. data/lib/capybara/queries/current_path_query.rb +8 -24
  24. data/lib/capybara/queries/match_query.rb +3 -7
  25. data/lib/capybara/queries/selector_query.rb +92 -95
  26. data/lib/capybara/queries/sibling_query.rb +4 -4
  27. data/lib/capybara/queries/text_query.rb +37 -34
  28. data/lib/capybara/queries/title_query.rb +8 -11
  29. data/lib/capybara/rack_test/browser.rb +15 -18
  30. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  31. data/lib/capybara/rack_test/driver.rb +6 -10
  32. data/lib/capybara/rack_test/form.rb +50 -40
  33. data/lib/capybara/rack_test/node.rb +70 -56
  34. data/lib/capybara/rails.rb +2 -6
  35. data/lib/capybara/result.rb +22 -22
  36. data/lib/capybara/rspec.rb +5 -10
  37. data/lib/capybara/rspec/compound.rb +5 -10
  38. data/lib/capybara/rspec/features.rb +17 -48
  39. data/lib/capybara/rspec/matcher_proxies.rb +31 -15
  40. data/lib/capybara/rspec/matchers.rb +70 -60
  41. data/lib/capybara/selector.rb +129 -117
  42. data/lib/capybara/selector/css.rb +6 -11
  43. data/lib/capybara/selector/filter.rb +1 -17
  44. data/lib/capybara/selector/filter_set.rb +17 -14
  45. data/lib/capybara/selector/filters/base.rb +7 -6
  46. data/lib/capybara/selector/filters/expression_filter.rb +6 -23
  47. data/lib/capybara/selector/filters/node_filter.rb +2 -12
  48. data/lib/capybara/selector/selector.rb +27 -33
  49. data/lib/capybara/selenium/driver.rb +113 -127
  50. data/lib/capybara/selenium/node.rb +148 -113
  51. data/lib/capybara/server.rb +3 -2
  52. data/lib/capybara/session.rb +137 -223
  53. data/lib/capybara/session/config.rb +47 -67
  54. data/lib/capybara/session/matchers.rb +8 -7
  55. data/lib/capybara/spec/public/test.js +26 -4
  56. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -0
  57. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -2
  58. data/lib/capybara/spec/session/accept_prompt_spec.rb +1 -0
  59. data/lib/capybara/spec/session/all_spec.rb +31 -18
  60. data/lib/capybara/spec/session/ancestor_spec.rb +2 -4
  61. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +6 -5
  62. data/lib/capybara/spec/session/assert_current_path.rb +12 -11
  63. data/lib/capybara/spec/session/assert_selector.rb +1 -0
  64. data/lib/capybara/spec/session/assert_text.rb +18 -17
  65. data/lib/capybara/spec/session/assert_title.rb +1 -0
  66. data/lib/capybara/spec/session/attach_file_spec.rb +14 -13
  67. data/lib/capybara/spec/session/body_spec.rb +1 -0
  68. data/lib/capybara/spec/session/check_spec.rb +7 -6
  69. data/lib/capybara/spec/session/choose_spec.rb +5 -4
  70. data/lib/capybara/spec/session/click_button_spec.rb +20 -28
  71. data/lib/capybara/spec/session/click_link_or_button_spec.rb +8 -7
  72. data/lib/capybara/spec/session/click_link_spec.rb +8 -7
  73. data/lib/capybara/spec/session/current_scope_spec.rb +4 -3
  74. data/lib/capybara/spec/session/current_url_spec.rb +7 -6
  75. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +1 -1
  76. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +1 -0
  77. data/lib/capybara/spec/session/element/assert_match_selector.rb +1 -1
  78. data/lib/capybara/spec/session/element/match_xpath_spec.rb +1 -1
  79. data/lib/capybara/spec/session/element/matches_selector_spec.rb +5 -5
  80. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +3 -2
  81. data/lib/capybara/spec/session/evaluate_script_spec.rb +4 -3
  82. data/lib/capybara/spec/session/execute_script_spec.rb +4 -3
  83. data/lib/capybara/spec/session/fill_in_spec.rb +6 -5
  84. data/lib/capybara/spec/session/find_button_spec.rb +4 -3
  85. data/lib/capybara/spec/session/find_by_id_spec.rb +2 -1
  86. data/lib/capybara/spec/session/find_field_spec.rb +8 -14
  87. data/lib/capybara/spec/session/find_link_spec.rb +6 -5
  88. data/lib/capybara/spec/session/find_spec.rb +37 -31
  89. data/lib/capybara/spec/session/first_spec.rb +60 -33
  90. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +2 -1
  91. data/lib/capybara/spec/session/frame/within_frame_spec.rb +9 -16
  92. data/lib/capybara/spec/session/go_back_spec.rb +1 -0
  93. data/lib/capybara/spec/session/go_forward_spec.rb +1 -0
  94. data/lib/capybara/spec/session/has_all_selectors_spec.rb +15 -15
  95. data/lib/capybara/spec/session/has_button_spec.rb +2 -1
  96. data/lib/capybara/spec/session/has_css_spec.rb +3 -2
  97. data/lib/capybara/spec/session/has_current_path_spec.rb +12 -28
  98. data/lib/capybara/spec/session/has_field_spec.rb +4 -3
  99. data/lib/capybara/spec/session/has_link_spec.rb +1 -0
  100. data/lib/capybara/spec/session/has_none_selectors_spec.rb +17 -17
  101. data/lib/capybara/spec/session/has_select_spec.rb +30 -29
  102. data/lib/capybara/spec/session/has_selector_spec.rb +5 -4
  103. data/lib/capybara/spec/session/has_table_spec.rb +2 -1
  104. data/lib/capybara/spec/session/has_text_spec.rb +6 -5
  105. data/lib/capybara/spec/session/has_title_spec.rb +1 -0
  106. data/lib/capybara/spec/session/has_xpath_spec.rb +1 -0
  107. data/lib/capybara/spec/session/headers.rb +2 -1
  108. data/lib/capybara/spec/session/html_spec.rb +1 -0
  109. data/lib/capybara/spec/session/node_spec.rb +91 -56
  110. data/lib/capybara/spec/session/node_wrapper_spec.rb +36 -0
  111. data/lib/capybara/spec/session/refresh_spec.rb +4 -2
  112. data/lib/capybara/spec/session/reset_session_spec.rb +1 -0
  113. data/lib/capybara/spec/session/response_code.rb +1 -0
  114. data/lib/capybara/spec/session/save_and_open_page_spec.rb +1 -0
  115. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +6 -11
  116. data/lib/capybara/spec/session/save_page_spec.rb +1 -17
  117. data/lib/capybara/spec/session/save_screenshot_spec.rb +1 -1
  118. data/lib/capybara/spec/session/select_spec.rb +20 -20
  119. data/lib/capybara/spec/session/selectors_spec.rb +2 -2
  120. data/lib/capybara/spec/session/sibling_spec.rb +1 -1
  121. data/lib/capybara/spec/session/text_spec.rb +1 -0
  122. data/lib/capybara/spec/session/title_spec.rb +1 -1
  123. data/lib/capybara/spec/session/uncheck_spec.rb +4 -3
  124. data/lib/capybara/spec/session/unselect_spec.rb +6 -5
  125. data/lib/capybara/spec/session/visit_spec.rb +9 -3
  126. data/lib/capybara/spec/session/window/become_closed_spec.rb +2 -1
  127. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -0
  128. data/lib/capybara/spec/session/window/open_new_window_spec.rb +1 -0
  129. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +2 -1
  130. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +2 -1
  131. data/lib/capybara/spec/session/window/window_spec.rb +12 -12
  132. data/lib/capybara/spec/session/window/windows_spec.rb +2 -3
  133. data/lib/capybara/spec/session/window/within_window_spec.rb +13 -68
  134. data/lib/capybara/spec/session/within_spec.rb +1 -0
  135. data/lib/capybara/spec/spec_helper.rb +26 -18
  136. data/lib/capybara/spec/test_app.rb +8 -9
  137. data/lib/capybara/spec/views/form.erb +1 -0
  138. data/lib/capybara/spec/views/with_html.erb +3 -1
  139. data/lib/capybara/spec/views/within_frames.erb +4 -1
  140. data/lib/capybara/version.rb +2 -1
  141. data/lib/capybara/window.rb +6 -10
  142. data/spec/basic_node_spec.rb +1 -0
  143. data/spec/capybara_spec.rb +9 -32
  144. data/spec/dsl_spec.rb +5 -13
  145. data/spec/filter_set_spec.rb +5 -4
  146. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -1
  147. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -2
  148. data/spec/minitest_spec.rb +4 -3
  149. data/spec/minitest_spec_spec.rb +3 -2
  150. data/spec/per_session_config_spec.rb +9 -8
  151. data/spec/rack_test_spec.rb +21 -20
  152. data/spec/result_spec.rb +17 -16
  153. data/spec/rspec/features_spec.rb +17 -14
  154. data/spec/rspec/scenarios_spec.rb +5 -7
  155. data/spec/rspec/shared_spec_matchers.rb +96 -99
  156. data/spec/rspec/views_spec.rb +2 -1
  157. data/spec/rspec_matchers_spec.rb +19 -2
  158. data/spec/rspec_spec.rb +11 -15
  159. data/spec/selector_spec.rb +5 -6
  160. data/spec/selenium_spec_chrome.rb +7 -4
  161. data/spec/selenium_spec_marionette.rb +26 -12
  162. data/spec/server_spec.rb +33 -33
  163. data/spec/session_spec.rb +2 -1
  164. data/spec/shared_selenium_session.rb +27 -21
  165. data/spec/spec_helper.rb +2 -5
  166. metadata +66 -87
  167. data/lib/capybara/query.rb +0 -7
  168. 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 << if value[0] =~ NMSTART
9
- value.slice!(0...1)
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
- return "\\%06x" % c.ord() unless c =~ %r{[ -/:-~]}
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
- 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
-
28
+ def description(**options)
29
+ opts = options_with_defaults(options)
33
30
  @descriptions.map do |desc|
34
- desc.call(options_with_defaults).to_s
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) }.freeze
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
- private
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, *types_and_options, &block)
68
- options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
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.has_key?(:default)
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.has_key?(:skip_if) && value == @options[:skip_if]
23
+ @options.key?(:skip_if) && value == @options[:skip_if]
23
24
  end
24
25
 
25
- private
26
+ private
26
27
 
27
28
  def valid_value?(value)
28
- !@options.has_key?(:valid_values) || Array(@options[:valid_values]).include?(value)
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
- "#{on}"
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
- options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
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
- options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
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 &block
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
- private
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, options={})
236
- locate_xpath = xpath #need to save original xpath for the label wrap
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 = XPath.attr(:id).equals(locator).or(
240
- XPath.attr(:name).equals(locator)).or(
241
- XPath.attr(:placeholder).equals(locator)).or(
242
- XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)))
243
- attr_matchers = attr_matchers.or XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
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.has_key?(ef) }.join
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).equals(value) : nil
262
+ value ? XPath.attr(attribute) == value : nil
263
263
  end
264
264
  end
265
265
 
266
266
  def find_by_class_attr(classes)
267
- if classes
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
- :browser => :firefox,
8
+ browser: :firefox,
8
9
  clear_local_storage: false,
9
10
  clear_session_storage: false
10
- }
11
- SPECIAL_OPTIONS = [:browser, :clear_local_storage, :clear_session_storage]
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].merge!({ unexpectedAlertBehaviour: "ignore" })
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 = $!.status if $!.is_a?(SystemExit)
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
- accept_modal(nil, wait: 0.1) do
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) ? arg.native : arg} )
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
- if @browser
112
- navigated = false
113
- start_time = Capybara::Helpers.monotonic_time
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
- if !navigated
116
- # Only trigger a navigation if we haven't done it already, otherwise it
117
- # can trigger an endless series of unload modals
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
- @browser.manage.delete_all_cookies
120
- if options[:clear_session_storage]
121
- if @browser.respond_to? :session_storage
122
- @browser.session_storage.clear
123
- else
124
- warn "sessionStorage clear requested but is not available for this driver"
125
- end
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 within_window(locator)
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
- [::Selenium::WebDriver::Error::StaleElementReferenceError,
268
- ::Selenium::WebDriver::Error::UnhandledError,
269
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
270
- ::Selenium::WebDriver::Error::InvalidSelectorError, # Work around a race condition that can occur with chromedriver and #go_back/#go_forward
271
- ::Selenium::WebDriver::Error::ElementNotInteractableError,
272
- ::Selenium::WebDriver::Error::ElementClickInterceptedError,
273
- ::Selenium::WebDriver::Error::InvalidElementStateError,
274
- ::Selenium::WebDriver::Error::ElementNotSelectableError,
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
- # @deprecated This method is being removed
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 = self.current_window_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 = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
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.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
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 = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s)
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.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
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.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}")
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
- [ /Error communicating with the remote browser/ ]
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
- begin
470
- require 'selenium-webdriver'
471
- # Fix for selenium-webdriver 3.4.0 which misnamed these
472
- if !defined?(::Selenium::WebDriver::Error::ElementNotInteractableError)
473
- ::Selenium::WebDriver::Error.const_set('ElementNotInteractableError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
474
- end
475
- if !defined?(::Selenium::WebDriver::Error::ElementClickInterceptedError)
476
- ::Selenium::WebDriver::Error.const_set('ElementClickInterceptedError', Class.new(::Selenium::WebDriver::Error::WebDriverError))
477
- end
478
- rescue LoadError => e
479
- if e.message =~ /selenium-webdriver/
480
- 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."
481
- else
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