capybara 3.16.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +321 -0
  4. data/README.md +51 -60
  5. data/lib/capybara.rb +71 -114
  6. data/lib/capybara/config.rb +8 -5
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/node.rb +15 -3
  9. data/lib/capybara/dsl.rb +10 -2
  10. data/lib/capybara/helpers.rb +5 -3
  11. data/lib/capybara/minitest.rb +242 -141
  12. data/lib/capybara/minitest/spec.rb +159 -90
  13. data/lib/capybara/node/actions.rb +85 -74
  14. data/lib/capybara/node/base.rb +4 -4
  15. data/lib/capybara/node/document.rb +2 -2
  16. data/lib/capybara/node/document_matchers.rb +3 -3
  17. data/lib/capybara/node/element.rb +216 -117
  18. data/lib/capybara/node/finders.rb +65 -65
  19. data/lib/capybara/node/matchers.rb +228 -126
  20. data/lib/capybara/node/simple.rb +9 -4
  21. data/lib/capybara/queries/ancestor_query.rb +5 -7
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +1 -1
  24. data/lib/capybara/queries/selector_query.rb +296 -30
  25. data/lib/capybara/queries/sibling_query.rb +5 -4
  26. data/lib/capybara/queries/style_query.rb +2 -2
  27. data/lib/capybara/queries/text_query.rb +13 -1
  28. data/lib/capybara/queries/title_query.rb +1 -1
  29. data/lib/capybara/rack_test/browser.rb +7 -2
  30. data/lib/capybara/rack_test/driver.rb +1 -1
  31. data/lib/capybara/rack_test/form.rb +1 -1
  32. data/lib/capybara/rack_test/node.rb +43 -7
  33. data/lib/capybara/registration_container.rb +44 -0
  34. data/lib/capybara/registrations/drivers.rb +36 -0
  35. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  36. data/lib/capybara/registrations/servers.rb +44 -0
  37. data/lib/capybara/result.rb +36 -8
  38. data/lib/capybara/rspec/matcher_proxies.rb +6 -4
  39. data/lib/capybara/rspec/matchers.rb +100 -63
  40. data/lib/capybara/rspec/matchers/base.rb +23 -10
  41. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  42. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  43. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  44. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  45. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  46. data/lib/capybara/rspec/matchers/have_text.rb +4 -4
  47. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  48. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  49. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  50. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  51. data/lib/capybara/selector.rb +219 -588
  52. data/lib/capybara/selector/builders/css_builder.rb +10 -6
  53. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  54. data/lib/capybara/selector/css.rb +4 -2
  55. data/lib/capybara/selector/definition.rb +277 -0
  56. data/lib/capybara/selector/definition/button.rb +52 -0
  57. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  58. data/lib/capybara/selector/definition/css.rb +10 -0
  59. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  60. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  61. data/lib/capybara/selector/definition/element.rb +27 -0
  62. data/lib/capybara/selector/definition/field.rb +40 -0
  63. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  64. data/lib/capybara/selector/definition/file_field.rb +13 -0
  65. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  66. data/lib/capybara/selector/definition/frame.rb +17 -0
  67. data/lib/capybara/selector/definition/id.rb +6 -0
  68. data/lib/capybara/selector/definition/label.rb +62 -0
  69. data/lib/capybara/selector/definition/link.rb +54 -0
  70. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  71. data/lib/capybara/selector/definition/option.rb +27 -0
  72. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  73. data/lib/capybara/selector/definition/select.rb +81 -0
  74. data/lib/capybara/selector/definition/table.rb +109 -0
  75. data/lib/capybara/selector/definition/table_row.rb +21 -0
  76. data/lib/capybara/selector/definition/xpath.rb +5 -0
  77. data/lib/capybara/selector/filter_set.rb +13 -9
  78. data/lib/capybara/selector/filters/base.rb +11 -2
  79. data/lib/capybara/selector/filters/locator_filter.rb +13 -3
  80. data/lib/capybara/selector/regexp_disassembler.rb +9 -2
  81. data/lib/capybara/selector/selector.rb +43 -448
  82. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  83. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  84. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  85. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  86. data/lib/capybara/selenium/driver.rb +125 -56
  87. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +73 -17
  88. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  89. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +41 -2
  90. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +14 -1
  91. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +14 -5
  92. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  93. data/lib/capybara/selenium/extensions/find.rb +67 -45
  94. data/lib/capybara/selenium/extensions/html5_drag.rb +152 -36
  95. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  96. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  97. data/lib/capybara/selenium/node.rb +227 -56
  98. data/lib/capybara/selenium/nodes/chrome_node.rb +93 -8
  99. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  100. data/lib/capybara/selenium/nodes/firefox_node.rb +37 -59
  101. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  102. data/lib/capybara/selenium/nodes/safari_node.rb +27 -54
  103. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  104. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  105. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  106. data/lib/capybara/selenium/patches/logs.rb +45 -0
  107. data/lib/capybara/server.rb +19 -3
  108. data/lib/capybara/server/animation_disabler.rb +2 -2
  109. data/lib/capybara/server/checker.rb +6 -2
  110. data/lib/capybara/server/middleware.rb +23 -13
  111. data/lib/capybara/session.rb +124 -106
  112. data/lib/capybara/session/config.rb +12 -10
  113. data/lib/capybara/session/matchers.rb +6 -6
  114. data/lib/capybara/spec/public/offset.js +6 -0
  115. data/lib/capybara/spec/public/test.js +94 -5
  116. data/lib/capybara/spec/session/all_spec.rb +84 -6
  117. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  118. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  119. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  120. data/lib/capybara/spec/session/attach_file_spec.rb +14 -6
  121. data/lib/capybara/spec/session/check_spec.rb +10 -4
  122. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  123. data/lib/capybara/spec/session/click_button_spec.rb +44 -1
  124. data/lib/capybara/spec/session/click_link_spec.rb +11 -0
  125. data/lib/capybara/spec/session/evaluate_script_spec.rb +12 -0
  126. data/lib/capybara/spec/session/fill_in_spec.rb +37 -2
  127. data/lib/capybara/spec/session/find_spec.rb +60 -6
  128. data/lib/capybara/spec/session/first_spec.rb +1 -1
  129. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +14 -1
  130. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  131. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  132. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  133. data/lib/capybara/spec/session/has_css_spec.rb +35 -6
  134. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  135. data/lib/capybara/spec/session/has_field_spec.rb +34 -0
  136. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  137. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  138. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  139. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  140. data/lib/capybara/spec/session/has_text_spec.rb +47 -0
  141. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  142. data/lib/capybara/spec/session/node_spec.rb +574 -16
  143. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  144. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  145. data/lib/capybara/spec/session/scroll_spec.rb +1 -1
  146. data/lib/capybara/spec/session/select_spec.rb +5 -10
  147. data/lib/capybara/spec/session/selectors_spec.rb +24 -3
  148. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  149. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  150. data/lib/capybara/spec/session/window/window_spec.rb +10 -9
  151. data/lib/capybara/spec/spec_helper.rb +7 -2
  152. data/lib/capybara/spec/test_app.rb +26 -21
  153. data/lib/capybara/spec/views/animated.erb +49 -0
  154. data/lib/capybara/spec/views/form.erb +25 -4
  155. data/lib/capybara/spec/views/frame_child.erb +2 -1
  156. data/lib/capybara/spec/views/frame_one.erb +1 -0
  157. data/lib/capybara/spec/views/obscured.erb +9 -9
  158. data/lib/capybara/spec/views/offset.erb +32 -0
  159. data/lib/capybara/spec/views/react.erb +45 -0
  160. data/lib/capybara/spec/views/spatial.erb +31 -0
  161. data/lib/capybara/spec/views/with_animation.erb +29 -1
  162. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  163. data/lib/capybara/spec/views/with_html.erb +28 -2
  164. data/lib/capybara/spec/views/with_js.erb +2 -1
  165. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  166. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  167. data/lib/capybara/version.rb +1 -1
  168. data/lib/capybara/window.rb +10 -10
  169. data/spec/basic_node_spec.rb +6 -6
  170. data/spec/capybara_spec.rb +28 -28
  171. data/spec/dsl_spec.rb +16 -3
  172. data/spec/filter_set_spec.rb +5 -5
  173. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  174. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  175. data/spec/minitest_spec.rb +12 -2
  176. data/spec/minitest_spec_spec.rb +56 -45
  177. data/spec/rack_test_spec.rb +25 -12
  178. data/spec/regexp_dissassembler_spec.rb +53 -39
  179. data/spec/result_spec.rb +50 -54
  180. data/spec/rspec/features_spec.rb +1 -0
  181. data/spec/rspec/shared_spec_matchers.rb +78 -62
  182. data/spec/rspec_spec.rb +5 -5
  183. data/spec/sauce_spec_chrome.rb +1 -0
  184. data/spec/selector_spec.rb +26 -16
  185. data/spec/selenium_spec_chrome.rb +84 -5
  186. data/spec/selenium_spec_chrome_remote.rb +23 -8
  187. data/spec/selenium_spec_edge.rb +23 -8
  188. data/spec/selenium_spec_firefox.rb +16 -21
  189. data/spec/selenium_spec_firefox_remote.rb +4 -13
  190. data/spec/selenium_spec_ie.rb +23 -15
  191. data/spec/selenium_spec_safari.rb +17 -17
  192. data/spec/server_spec.rb +87 -42
  193. data/spec/session_spec.rb +11 -4
  194. data/spec/shared_selenium_node.rb +83 -0
  195. data/spec/shared_selenium_session.rb +62 -72
  196. data/spec/spec_helper.rb +43 -5
  197. metadata +114 -16
@@ -17,11 +17,7 @@ module Capybara
17
17
  conditions = if name == :class
18
18
  class_conditions(value)
19
19
  elsif value.is_a? Regexp
20
- Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
21
- strs.map do |str|
22
- "[#{name}*='#{str}'#{' i' if value.casefold?}]"
23
- end.join
24
- end
20
+ regexp_conditions(name, value)
25
21
  else
26
22
  [attribute_conditions(name => value)]
27
23
  end
@@ -36,6 +32,14 @@ module Capybara
36
32
 
37
33
  private
38
34
 
35
+ def regexp_conditions(name, value)
36
+ Selector::RegexpDisassembler.new(value).alternated_substrings.map do |strs|
37
+ strs.map do |str|
38
+ "[#{name}*='#{str}'#{' i' if value.casefold?}]"
39
+ end.join
40
+ end
41
+ end
42
+
39
43
  def attribute_conditions(attributes)
40
44
  attributes.map do |attribute, value|
41
45
  case value
@@ -70,7 +74,7 @@ module Capybara
70
74
  end.join
71
75
  end
72
76
  else
73
- cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
77
+ cls = Array(classes).group_by { |cl| cl.match?(/^!(?!!!)/) }
74
78
  [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
75
79
  cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
76
80
  end
@@ -48,7 +48,7 @@ module Capybara
48
48
  attribute_conditions(class: classes)
49
49
  else
50
50
  Array(classes).map do |klass|
51
- if klass.start_with?('!') && !klass.start_with?('!!!')
51
+ if klass.match?(/^!(?!!!)/)
52
52
  !XPath.attr(:class).contains_word(klass.slice(1..-1))
53
53
  else
54
54
  XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selector/selector'
4
+
3
5
  module Capybara
4
6
  class Selector
5
7
  class CSS
6
8
  def self.escape(str)
7
9
  value = str.dup
8
10
  out = +''
9
- out << value.slice!(0...1) if value =~ /^[-_]/
11
+ out << value.slice!(0...1) if value.match?(/^[-_]/)
10
12
  out << (value[0].match?(NMSTART) ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
11
13
  out << value.gsub(/[^a-zA-Z0-9_-]/) { |char| escape_char char }
12
14
  out
13
15
  end
14
16
 
15
17
  def self.escape_char(char)
16
- char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06x', char.ord)
18
+ char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06<hex>x', hex: char.ord)
17
19
  end
18
20
 
19
21
  def self.split(css)
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/filter_set'
4
+ require 'capybara/selector/css'
5
+ require 'capybara/selector/regexp_disassembler'
6
+ require 'capybara/selector/builders/xpath_builder'
7
+ require 'capybara/selector/builders/css_builder'
8
+
9
+ module Capybara
10
+ class Selector
11
+ class Definition
12
+ attr_reader :name, :expressions
13
+
14
+ extend Forwardable
15
+
16
+ def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
17
+ @name = name
18
+ @filter_set = Capybara::Selector::FilterSet.add(name) {}
19
+ @match = nil
20
+ @label = nil
21
+ @failure_message = nil
22
+ @expressions = {}
23
+ @expression_filters = {}
24
+ @locator_filter = nil
25
+ @default_visibility = nil
26
+ @locator_type = locator_type
27
+ @raw_locator = raw_locator
28
+ @supports_exact = supports_exact
29
+ instance_eval(&block)
30
+ end
31
+
32
+ def custom_filters
33
+ warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
34
+ node_filters.merge(expression_filters).freeze
35
+ end
36
+
37
+ def node_filters
38
+ @filter_set.node_filters
39
+ end
40
+
41
+ def expression_filters
42
+ @filter_set.expression_filters
43
+ end
44
+
45
+ ##
46
+ #
47
+ # Define a selector by an xpath expression
48
+ #
49
+ # @overload xpath(*expression_filters, &block)
50
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that are implemented via this expression, if not specified the names of any keyword parameters in the block will be used
51
+ # @yield [locator, options] The block to use to generate the XPath expression
52
+ # @yieldparam [String] locator The locator string passed to the query
53
+ # @yieldparam [Hash] options The options hash passed to the query
54
+ # @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
55
+ #
56
+ # @overload xpath()
57
+ # @return [#call] The block that will be called to generate the XPath expression
58
+ #
59
+ def xpath(*allowed_filters, &block)
60
+ expression(:xpath, allowed_filters, &block)
61
+ end
62
+
63
+ ##
64
+ #
65
+ # Define a selector by a CSS selector
66
+ #
67
+ # @overload css(*expression_filters, &block)
68
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
69
+ # @yield [locator, options] The block to use to generate the CSS selector
70
+ # @yieldparam [String] locator The locator string passed to the query
71
+ # @yieldparam [Hash] options The options hash passed to the query
72
+ # @yieldreturn [#to_s] An object that can produce a CSS selector
73
+ #
74
+ # @overload css()
75
+ # @return [#call] The block that will be called to generate the CSS selector
76
+ #
77
+ def css(*allowed_filters, &block)
78
+ expression(:css, allowed_filters, &block)
79
+ end
80
+
81
+ ##
82
+ #
83
+ # Automatic selector detection
84
+ #
85
+ # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
86
+ # @yieldparam [String], locator The locator string used to determin if it matches the selector
87
+ # @yieldreturn [Boolean] Whether this selector matches the locator string
88
+ # @return [#call] The block that will be used to detect selector match
89
+ #
90
+ def match(&block)
91
+ @match = block if block
92
+ @match
93
+ end
94
+
95
+ ##
96
+ #
97
+ # Set/get a descriptive label for the selector
98
+ #
99
+ # @overload label(label)
100
+ # @param [String] label A descriptive label for this selector - used in error messages
101
+ # @overload label()
102
+ # @return [String] The currently set label
103
+ #
104
+ def label(label = nil)
105
+ @label = label if label
106
+ @label
107
+ end
108
+
109
+ ##
110
+ #
111
+ # Description of the selector
112
+ #
113
+ # @!method description(options)
114
+ # @param [Hash] options The options of the query used to generate the description
115
+ # @return [String] Description of the selector when used with the options passed
116
+ def_delegator :@filter_set, :description
117
+
118
+ ##
119
+ #
120
+ # Should this selector be used for the passed in locator
121
+ #
122
+ # This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
123
+ #
124
+ # @param [String] locator The locator passed to the query
125
+ # @return [Boolean] Whether or not to use this selector
126
+ #
127
+ def match?(locator)
128
+ @match&.call(locator)
129
+ end
130
+
131
+ ##
132
+ #
133
+ # Define a node filter for use with this selector
134
+ #
135
+ # @!method node_filter(name, *types, options={}, &block)
136
+ # @param [Symbol, Regexp] name The filter name
137
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
138
+ # @param [Hash] options ({}) Options of the filter
139
+ # @option options [Array<>] :valid_values Valid values for this filter
140
+ # @option options :default The default value of the filter (if any)
141
+ # @option options :skip_if Value of the filter that will cause it to be skipped
142
+ # @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
143
+ #
144
+ # If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
145
+ # is passed for the name the block should accept | node, option_name, option_value |. In either case
146
+ # the block should return `true` if the node passes the filer or `false` if it doesn't
147
+
148
+ ##
149
+ #
150
+ # Define an expression filter for use with this selector
151
+ #
152
+ # @!method expression_filter(name, *types, matcher: nil, **options, &block)
153
+ # @param [Symbol, Regexp] name The filter name
154
+ # @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
155
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
156
+ # @param [Hash] options ({}) Options of the filter
157
+ # @option options [Array<>] :valid_values Valid values for this filter
158
+ # @option options :default The default value of the filter (if any)
159
+ # @option options :skip_if Value of the filter that will cause it to be skipped
160
+ # @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
161
+ #
162
+ # If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
163
+ # is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
164
+ # the block should return the modified expression
165
+
166
+ def_delegators :@filter_set, :node_filter, :expression_filter, :filter
167
+
168
+ def locator_filter(*types, **options, &block)
169
+ types.each { |type| options[type] = true }
170
+ @locator_filter = Capybara::Selector::Filters::LocatorFilter.new(block, **options) if block
171
+ @locator_filter
172
+ end
173
+
174
+ def filter_set(name, filters_to_use = nil)
175
+ @filter_set.import(name, filters_to_use)
176
+ end
177
+
178
+ def_delegator :@filter_set, :describe
179
+
180
+ def describe_expression_filters(&block)
181
+ if block_given?
182
+ describe(:expression_filters, &block)
183
+ else
184
+ describe(:expression_filters) do |**options|
185
+ describe_all_expression_filters(**options)
186
+ end
187
+ end
188
+ end
189
+
190
+ def describe_all_expression_filters(**opts)
191
+ expression_filters.map do |ef_name, ef|
192
+ if ef.matcher?
193
+ handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
194
+ elsif opts.key?(ef_name)
195
+ " with #{ef_name} #{opts[ef_name]}"
196
+ end
197
+ end.join
198
+ end
199
+
200
+ def describe_node_filters(&block)
201
+ describe(:node_filters, &block)
202
+ end
203
+
204
+ ##
205
+ #
206
+ # Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
207
+ # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
208
+ #
209
+ # @param [Symbol] default_visibility Only find elements with the specified visibility:
210
+ # * :all - finds visible and invisible elements.
211
+ # * :hidden - only finds invisible elements.
212
+ # * :visible - only finds visible elements.
213
+ def visible(default_visibility = nil, &block)
214
+ @default_visibility = block || default_visibility
215
+ end
216
+
217
+ def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
218
+ vis = if @default_visibility&.respond_to?(:call)
219
+ @default_visibility.call(options)
220
+ else
221
+ @default_visibility
222
+ end
223
+ vis.nil? ? fallback : vis
224
+ end
225
+
226
+ # @api private
227
+ def raw_locator?
228
+ !!@raw_locator
229
+ end
230
+
231
+ # @api private
232
+ def supports_exact?
233
+ @supports_exact
234
+ end
235
+
236
+ def default_format
237
+ return nil if @expressions.keys.empty?
238
+
239
+ if @expressions.size == 1
240
+ @expressions.keys.first
241
+ else
242
+ :xpath
243
+ end
244
+ end
245
+
246
+ # @api private
247
+ def locator_types
248
+ return nil unless @locator_type
249
+
250
+ Array(@locator_type)
251
+ end
252
+
253
+ private
254
+
255
+ def handled_custom_options(filter, options)
256
+ options.select do |option, _|
257
+ filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
258
+ end
259
+ end
260
+
261
+ def parameter_names(block)
262
+ block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
263
+ end
264
+
265
+ def expression(type, allowed_filters, &block)
266
+ if block
267
+ @expressions[type] = block
268
+ allowed_filters = parameter_names(block) if allowed_filters.empty?
269
+ allowed_filters.flatten.each do |ef|
270
+ expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
271
+ end
272
+ end
273
+ @expressions[type]
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
+ xpath(:value, :title, :type, :name) do |locator, **options|
5
+ input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
+ btn_xpath = XPath.descendant(:button)
7
+ btn_xpath += XPath.descendant[XPath.attr(:role).equals('button')] if enable_aria_role
8
+ image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
9
+
10
+ unless locator.nil?
11
+ locator = locator.to_s
12
+ locator_matchers = XPath.attr(:id).equals(locator) |
13
+ XPath.attr(:name).equals(locator) |
14
+ XPath.attr(:value).is(locator) |
15
+ XPath.attr(:title).is(locator)
16
+ locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
17
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
18
+
19
+ input_btn_xpath = input_btn_xpath[locator_matchers]
20
+
21
+ btn_xpath = btn_xpath[locator_matchers |
22
+ XPath.string.n.is(locator) |
23
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
24
+
25
+ alt_matches = XPath.attr(:alt).is(locator)
26
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
27
+ image_btn_xpath = image_btn_xpath[alt_matches]
28
+ end
29
+
30
+ %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
31
+ memo[find_by_attr(ef, options[ef])]
32
+ end
33
+ end
34
+
35
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
36
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
37
+
38
+ node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
39
+ expression_filter(:name) do |xpath, val|
40
+ builder(xpath).add_attribute_conditions(name: val)
41
+ end
42
+
43
+ describe_expression_filters do |disabled: nil, **options|
44
+ desc = +''
45
+ desc << ' that is not disabled' if disabled == false
46
+ desc << describe_all_expression_filters(**options)
47
+ end
48
+
49
+ describe_node_filters do |disabled: nil, **|
50
+ ' that is disabled' if disabled == true
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
4
+ xpath do |locator, allow_self: nil, **options|
5
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ XPath.attr(:type) == 'checkbox'
7
+ ]
8
+ locate_field(xpath, locator, **options)
9
+ end
10
+
11
+ filter_set(:_field, %i[checked unchecked disabled name])
12
+
13
+ node_filter(%i[option with]) do |node, value|
14
+ val = node.value
15
+ (value.is_a?(Regexp) ? value.match?(val) : val == value.to_s).tap do |res|
16
+ add_error("Expected value to be #{value.inspect} but it was #{val.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ describe_node_filters do |option: nil, with: nil, **|
21
+ desc = +''
22
+ desc << " with value #{option.inspect}" if option
23
+ desc << " with value #{with.inspect}" if with
24
+ desc
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
+ css do |css|
5
+ if css.is_a? Symbol
6
+ warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead."
7
+ end
8
+ css
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
4
+ label 'input box with datalist completion'
5
+
6
+ xpath do |locator, **options|
7
+ xpath = XPath.descendant(:input)[XPath.attr(:list)]
8
+ locate_field(xpath, locator, **options)
9
+ end
10
+
11
+ filter_set(:_field, %i[disabled name placeholder])
12
+
13
+ node_filter(:options) do |node, options|
14
+ actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
15
+ (options.sort == actual.sort).tap do |res|
16
+ add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ expression_filter(:with_options) do |expr, options|
21
+ options.inject(expr) do |xpath, option|
22
+ xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
23
+ end
24
+ end
25
+
26
+ describe_expression_filters do |with_options: nil, **|
27
+ desc = +''
28
+ desc << " with at least options #{with_options.inspect}" if with_options
29
+ desc
30
+ end
31
+
32
+ describe_node_filters do |options: nil, **|
33
+ " with options #{options.inspect}" if options
34
+ end
35
+ end