capybara 3.29.0 → 3.37.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +229 -15
  3. data/README.md +13 -4
  4. data/lib/capybara/config.rb +24 -10
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/driver/base.rb +8 -0
  7. data/lib/capybara/driver/node.rb +5 -1
  8. data/lib/capybara/dsl.rb +5 -3
  9. data/lib/capybara/helpers.rb +19 -2
  10. data/lib/capybara/minitest/spec.rb +156 -97
  11. data/lib/capybara/minitest.rb +232 -144
  12. data/lib/capybara/node/actions.rb +41 -37
  13. data/lib/capybara/node/base.rb +6 -6
  14. data/lib/capybara/node/document.rb +2 -2
  15. data/lib/capybara/node/document_matchers.rb +3 -3
  16. data/lib/capybara/node/element.rb +35 -21
  17. data/lib/capybara/node/finders.rb +33 -19
  18. data/lib/capybara/node/matchers.rb +72 -57
  19. data/lib/capybara/node/simple.rb +13 -3
  20. data/lib/capybara/queries/active_element_query.rb +18 -0
  21. data/lib/capybara/queries/ancestor_query.rb +4 -3
  22. data/lib/capybara/queries/base_query.rb +2 -1
  23. data/lib/capybara/queries/current_path_query.rb +14 -4
  24. data/lib/capybara/queries/selector_query.rb +91 -30
  25. data/lib/capybara/queries/sibling_query.rb +4 -3
  26. data/lib/capybara/queries/style_query.rb +1 -1
  27. data/lib/capybara/queries/text_query.rb +7 -1
  28. data/lib/capybara/rack_test/browser.rb +68 -10
  29. data/lib/capybara/rack_test/driver.rb +6 -5
  30. data/lib/capybara/rack_test/form.rb +2 -2
  31. data/lib/capybara/rack_test/node.rb +44 -16
  32. data/lib/capybara/registration_container.rb +41 -0
  33. data/lib/capybara/registrations/drivers.rb +18 -12
  34. data/lib/capybara/registrations/patches/puma_ssl.rb +3 -1
  35. data/lib/capybara/registrations/servers.rb +3 -2
  36. data/lib/capybara/result.rb +35 -15
  37. data/lib/capybara/rspec/matcher_proxies.rb +8 -8
  38. data/lib/capybara/rspec/matchers/base.rb +12 -6
  39. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  40. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  41. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  42. data/lib/capybara/rspec/matchers/have_selector.rb +16 -8
  43. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  44. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  45. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  46. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  47. data/lib/capybara/rspec/matchers/match_style.rb +7 -2
  48. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  49. data/lib/capybara/rspec/matchers.rb +33 -32
  50. data/lib/capybara/rspec.rb +2 -0
  51. data/lib/capybara/selector/builders/css_builder.rb +2 -2
  52. data/lib/capybara/selector/builders/xpath_builder.rb +4 -2
  53. data/lib/capybara/selector/css.rb +2 -2
  54. data/lib/capybara/selector/definition/button.rb +35 -13
  55. data/lib/capybara/selector/definition/checkbox.rb +3 -3
  56. data/lib/capybara/selector/definition/css.rb +3 -1
  57. data/lib/capybara/selector/definition/datalist_input.rb +2 -2
  58. data/lib/capybara/selector/definition/datalist_option.rb +1 -1
  59. data/lib/capybara/selector/definition/element.rb +3 -2
  60. data/lib/capybara/selector/definition/field.rb +1 -1
  61. data/lib/capybara/selector/definition/file_field.rb +2 -2
  62. data/lib/capybara/selector/definition/fillable_field.rb +3 -3
  63. data/lib/capybara/selector/definition/label.rb +5 -3
  64. data/lib/capybara/selector/definition/link.rb +8 -0
  65. data/lib/capybara/selector/definition/radio_button.rb +3 -3
  66. data/lib/capybara/selector/definition/select.rb +33 -14
  67. data/lib/capybara/selector/definition/table.rb +6 -3
  68. data/lib/capybara/selector/definition/table_row.rb +2 -2
  69. data/lib/capybara/selector/definition.rb +15 -11
  70. data/lib/capybara/selector/filter_set.rb +17 -17
  71. data/lib/capybara/selector/filters/base.rb +6 -1
  72. data/lib/capybara/selector/filters/locator_filter.rb +1 -1
  73. data/lib/capybara/selector/selector.rb +17 -3
  74. data/lib/capybara/selector.rb +37 -19
  75. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  76. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  77. data/lib/capybara/selenium/atoms/src/isDisplayed.js +1 -1
  78. data/lib/capybara/selenium/driver.rb +84 -17
  79. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +11 -13
  80. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +10 -12
  81. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +4 -4
  82. data/lib/capybara/selenium/extensions/find.rb +4 -4
  83. data/lib/capybara/selenium/extensions/html5_drag.rb +30 -13
  84. data/lib/capybara/selenium/extensions/scroll.rb +8 -10
  85. data/lib/capybara/selenium/logger_suppressor.rb +8 -2
  86. data/lib/capybara/selenium/node.rb +122 -26
  87. data/lib/capybara/selenium/nodes/chrome_node.rb +34 -19
  88. data/lib/capybara/selenium/nodes/edge_node.rb +5 -3
  89. data/lib/capybara/selenium/nodes/firefox_node.rb +11 -6
  90. data/lib/capybara/selenium/nodes/safari_node.rb +3 -3
  91. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  92. data/lib/capybara/selenium/patches/atoms.rb +4 -4
  93. data/lib/capybara/selenium/patches/logs.rb +7 -9
  94. data/lib/capybara/server/animation_disabler.rb +38 -15
  95. data/lib/capybara/server/checker.rb +1 -1
  96. data/lib/capybara/server/middleware.rb +22 -10
  97. data/lib/capybara/server.rb +15 -3
  98. data/lib/capybara/session/config.rb +10 -4
  99. data/lib/capybara/session/matchers.rb +11 -11
  100. data/lib/capybara/session.rb +62 -39
  101. data/lib/capybara/spec/public/test.js +75 -7
  102. data/lib/capybara/spec/session/accept_alert_spec.rb +1 -1
  103. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  104. data/lib/capybara/spec/session/all_spec.rb +63 -12
  105. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  106. data/lib/capybara/spec/session/assert_text_spec.rb +26 -22
  107. data/lib/capybara/spec/session/check_spec.rb +15 -0
  108. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  109. data/lib/capybara/spec/session/click_button_spec.rb +16 -0
  110. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  111. data/lib/capybara/spec/session/current_url_spec.rb +11 -1
  112. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  113. data/lib/capybara/spec/session/find_spec.rb +37 -8
  114. data/lib/capybara/spec/session/has_any_selectors_spec.rb +4 -0
  115. data/lib/capybara/spec/session/has_button_spec.rb +75 -0
  116. data/lib/capybara/spec/session/has_css_spec.rb +14 -10
  117. data/lib/capybara/spec/session/has_current_path_spec.rb +17 -4
  118. data/lib/capybara/spec/session/has_field_spec.rb +41 -1
  119. data/lib/capybara/spec/session/has_link_spec.rb +30 -0
  120. data/lib/capybara/spec/session/has_select_spec.rb +36 -8
  121. data/lib/capybara/spec/session/has_selector_spec.rb +19 -4
  122. data/lib/capybara/spec/session/has_table_spec.rb +51 -5
  123. data/lib/capybara/spec/session/has_text_spec.rb +21 -1
  124. data/lib/capybara/spec/session/html_spec.rb +1 -1
  125. data/lib/capybara/spec/session/matches_style_spec.rb +2 -2
  126. data/lib/capybara/spec/session/node_spec.rb +226 -33
  127. data/lib/capybara/spec/session/refresh_spec.rb +2 -1
  128. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  129. data/lib/capybara/spec/session/save_page_spec.rb +4 -4
  130. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  131. data/lib/capybara/spec/session/scroll_spec.rb +4 -4
  132. data/lib/capybara/spec/session/selectors_spec.rb +15 -2
  133. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  134. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +1 -1
  135. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +1 -1
  136. data/lib/capybara/spec/session/window/window_spec.rb +9 -9
  137. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  138. data/lib/capybara/spec/spec_helper.rb +17 -17
  139. data/lib/capybara/spec/test_app.rb +89 -29
  140. data/lib/capybara/spec/views/animated.erb +1 -1
  141. data/lib/capybara/spec/views/form.erb +52 -6
  142. data/lib/capybara/spec/views/frame_child.erb +1 -1
  143. data/lib/capybara/spec/views/frame_one.erb +1 -1
  144. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  145. data/lib/capybara/spec/views/frame_two.erb +1 -1
  146. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  147. data/lib/capybara/spec/views/layout.erb +10 -0
  148. data/lib/capybara/spec/views/obscured.erb +1 -1
  149. data/lib/capybara/spec/views/offset.erb +2 -1
  150. data/lib/capybara/spec/views/path.erb +2 -2
  151. data/lib/capybara/spec/views/popup_one.erb +1 -1
  152. data/lib/capybara/spec/views/popup_two.erb +1 -1
  153. data/lib/capybara/spec/views/react.erb +2 -2
  154. data/lib/capybara/spec/views/scroll.erb +2 -1
  155. data/lib/capybara/spec/views/spatial.erb +1 -1
  156. data/lib/capybara/spec/views/with_animation.erb +10 -3
  157. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  158. data/lib/capybara/spec/views/with_dragula.erb +5 -3
  159. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  160. data/lib/capybara/spec/views/with_hover.erb +2 -2
  161. data/lib/capybara/spec/views/with_html.erb +3 -3
  162. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  163. data/lib/capybara/spec/views/with_js.erb +5 -3
  164. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  165. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  166. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  167. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  168. data/lib/capybara/spec/views/with_sortable_js.erb +3 -3
  169. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  170. data/lib/capybara/spec/views/with_windows.erb +1 -1
  171. data/lib/capybara/spec/views/within_frames.erb +1 -1
  172. data/lib/capybara/version.rb +1 -1
  173. data/lib/capybara/window.rb +4 -8
  174. data/lib/capybara.rb +36 -29
  175. data/spec/basic_node_spec.rb +25 -11
  176. data/spec/capybara_spec.rb +1 -1
  177. data/spec/dsl_spec.rb +18 -5
  178. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  179. data/spec/fixtures/selenium_driver_rspec_success.rb +3 -3
  180. data/spec/minitest_spec.rb +3 -2
  181. data/spec/minitest_spec_spec.rb +46 -46
  182. data/spec/rack_test_spec.rb +43 -11
  183. data/spec/regexp_dissassembler_spec.rb +40 -36
  184. data/spec/result_spec.rb +53 -45
  185. data/spec/rspec/features_spec.rb +7 -4
  186. data/spec/rspec/scenarios_spec.rb +5 -1
  187. data/spec/rspec/shared_spec_matchers.rb +68 -56
  188. data/spec/rspec_spec.rb +8 -4
  189. data/spec/sauce_spec_chrome.rb +3 -3
  190. data/spec/selector_spec.rb +19 -4
  191. data/spec/selenium_spec_chrome.rb +49 -26
  192. data/spec/selenium_spec_chrome_remote.rb +13 -6
  193. data/spec/selenium_spec_firefox.rb +29 -17
  194. data/spec/selenium_spec_firefox_remote.rb +2 -2
  195. data/spec/selenium_spec_ie.rb +3 -6
  196. data/spec/selenium_spec_safari.rb +31 -19
  197. data/spec/server_spec.rb +88 -35
  198. data/spec/session_spec.rb +1 -1
  199. data/spec/shared_selenium_node.rb +21 -7
  200. data/spec/shared_selenium_session.rb +123 -21
  201. data/spec/spec_helper.rb +2 -2
  202. metadata +80 -21
  203. data/lib/capybara/spec/session/source_spec.rb +0 -0
  204. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -15,45 +15,45 @@ module Capybara
15
15
  # RSpec matcher for whether the element(s) matching a given selector exist.
16
16
  #
17
17
  # @see Capybara::Node::Matchers#assert_selector
18
- def have_selector(*args, &optional_filter_block)
19
- Matchers::HaveSelector.new(*args, &optional_filter_block)
18
+ def have_selector(...)
19
+ Matchers::HaveSelector.new(...)
20
20
  end
21
21
 
22
22
  # RSpec matcher for whether the element(s) matching a group of selectors exist.
23
23
  #
24
24
  # @see Capybara::Node::Matchers#assert_all_of_selectors
25
- def have_all_of_selectors(*args, &optional_filter_block)
26
- Matchers::HaveAllSelectors.new(*args, &optional_filter_block)
25
+ def have_all_of_selectors(...)
26
+ Matchers::HaveAllSelectors.new(...)
27
27
  end
28
28
 
29
29
  # RSpec matcher for whether no element(s) matching a group of selectors exist.
30
30
  #
31
31
  # @see Capybara::Node::Matchers#assert_none_of_selectors
32
- def have_none_of_selectors(*args, &optional_filter_block)
33
- Matchers::HaveNoSelectors.new(*args, &optional_filter_block)
32
+ def have_none_of_selectors(...)
33
+ Matchers::HaveNoSelectors.new(...)
34
34
  end
35
35
 
36
36
  # RSpec matcher for whether the element(s) matching any of a group of selectors exist.
37
37
  #
38
38
  # @see Capybara::Node::Matchers#assert_any_of_selectors
39
- def have_any_of_selectors(*args, &optional_filter_block)
40
- Matchers::HaveAnySelectors.new(*args, &optional_filter_block)
39
+ def have_any_of_selectors(...)
40
+ Matchers::HaveAnySelectors.new(...)
41
41
  end
42
42
 
43
43
  # RSpec matcher for whether the current element matches a given selector.
44
44
  #
45
45
  # @see Capybara::Node::Matchers#assert_matches_selector
46
- def match_selector(*args, &optional_filter_block)
47
- Matchers::MatchSelector.new(*args, &optional_filter_block)
46
+ def match_selector(...)
47
+ Matchers::MatchSelector.new(...)
48
48
  end
49
49
 
50
50
  %i[css xpath].each do |selector|
51
51
  define_method "have_#{selector}" do |expr, **options, &optional_filter_block|
52
- Matchers::HaveSelector.new(selector, expr, options, &optional_filter_block)
52
+ Matchers::HaveSelector.new(selector, expr, **options, &optional_filter_block)
53
53
  end
54
54
 
55
55
  define_method "match_#{selector}" do |expr, **options, &optional_filter_block|
56
- Matchers::MatchSelector.new(selector, expr, options, &optional_filter_block)
56
+ Matchers::MatchSelector.new(selector, expr, **options, &optional_filter_block)
57
57
  end
58
58
  end
59
59
 
@@ -79,7 +79,7 @@ module Capybara
79
79
 
80
80
  %i[link button field select table].each do |selector|
81
81
  define_method "have_#{selector}" do |locator = nil, **options, &optional_filter_block|
82
- Matchers::HaveSelector.new(selector, locator, options, &optional_filter_block)
82
+ Matchers::HaveSelector.new(selector, locator, **options, &optional_filter_block)
83
83
  end
84
84
  end
85
85
 
@@ -94,7 +94,7 @@ module Capybara
94
94
  # @see Capybara::Node::Matchers#has_button?
95
95
 
96
96
  # @!method have_field(locator = nil, **options, &optional_filter_block)
97
- # RSpec matcher for links.
97
+ # RSpec matcher for form fields.
98
98
  #
99
99
  # @see Capybara::Node::Matchers#has_field?
100
100
 
@@ -110,7 +110,7 @@ module Capybara
110
110
 
111
111
  %i[checked unchecked].each do |state|
112
112
  define_method "have_#{state}_field" do |locator = nil, **options, &optional_filter_block|
113
- Matchers::HaveSelector.new(:field, locator, options.merge(state => true), &optional_filter_block)
113
+ Matchers::HaveSelector.new(:field, locator, **options.merge(state => true), &optional_filter_block)
114
114
  end
115
115
  end
116
116
 
@@ -127,64 +127,65 @@ module Capybara
127
127
  # RSpec matcher for text content.
128
128
  #
129
129
  # @see Capybara::Node::Matchers#assert_text
130
- def have_text(*args)
131
- Matchers::HaveText.new(*args)
130
+ def have_text(text_or_type, *args, **options)
131
+ Matchers::HaveText.new(text_or_type, *args, **options)
132
132
  end
133
133
  alias_method :have_content, :have_text
134
134
 
135
135
  def have_title(title, **options)
136
- Matchers::HaveTitle.new(title, options)
136
+ Matchers::HaveTitle.new(title, **options)
137
137
  end
138
138
 
139
139
  # RSpec matcher for the current path.
140
140
  #
141
141
  # @see Capybara::SessionMatchers#assert_current_path
142
- def have_current_path(path, **options)
143
- Matchers::HaveCurrentPath.new(path, options)
142
+ def have_current_path(path, **options, &optional_filter_block)
143
+ Matchers::HaveCurrentPath.new(path, **options, &optional_filter_block)
144
144
  end
145
145
 
146
146
  # RSpec matcher for element style.
147
147
  #
148
148
  # @see Capybara::Node::Matchers#matches_style?
149
- def match_style(styles, **options)
150
- Matchers::MatchStyle.new(styles, options)
149
+ def match_style(styles = nil, **options)
150
+ styles, options = options, {} if styles.nil?
151
+ Matchers::MatchStyle.new(styles, **options)
151
152
  end
152
153
 
153
154
  ##
154
155
  # @deprecated
155
156
  #
156
- def have_style(styles, **options)
157
- warn 'DEPRECATED: have_style is deprecated, please use match_style'
157
+ def have_style(styles = nil, **options)
158
+ Capybara::Helpers.warn "DEPRECATED: have_style is deprecated, please use match_style : #{Capybara::Helpers.filter_backtrace(caller)}"
158
159
  match_style(styles, **options)
159
160
  end
160
161
 
161
162
  %w[selector css xpath text title current_path link button
162
163
  field checked_field unchecked_field select table
163
164
  sibling ancestor].each do |matcher_type|
164
- define_method "have_no_#{matcher_type}" do |*args, &optional_filter_block|
165
- Matchers::NegatedMatcher.new(send("have_#{matcher_type}", *args, &optional_filter_block))
165
+ define_method "have_no_#{matcher_type}" do |*args, **kw_args, &optional_filter_block|
166
+ Matchers::NegatedMatcher.new(send("have_#{matcher_type}", *args, **kw_args, &optional_filter_block))
166
167
  end
167
168
  end
168
169
  alias_method :have_no_content, :have_no_text
169
170
 
170
171
  %w[selector css xpath].each do |matcher_type|
171
- define_method "not_match_#{matcher_type}" do |*args, &optional_filter_block|
172
- Matchers::NegatedMatcher.new(send("match_#{matcher_type}", *args, &optional_filter_block))
172
+ define_method "not_match_#{matcher_type}" do |*args, **kw_args, &optional_filter_block|
173
+ Matchers::NegatedMatcher.new(send("match_#{matcher_type}", *args, **kw_args, &optional_filter_block))
173
174
  end
174
175
  end
175
176
 
176
177
  # RSpec matcher for whether sibling element(s) matching a given selector exist.
177
178
  #
178
179
  # @see Capybara::Node::Matchers#assert_sibling
179
- def have_sibling(*args, &optional_filter_block)
180
- Matchers::HaveSibling.new(*args, &optional_filter_block)
180
+ def have_sibling(...)
181
+ Matchers::HaveSibling.new(...)
181
182
  end
182
183
 
183
184
  # RSpec matcher for whether ancestor element(s) matching a given selector exist.
184
185
  #
185
186
  # @see Capybara::Node::Matchers#assert_ancestor
186
- def have_ancestor(*args, &optional_filter_block)
187
- Matchers::HaveAncestor.new(*args, &optional_filter_block)
187
+ def have_ancestor(...)
188
+ Matchers::HaveAncestor.new(...)
188
189
  end
189
190
 
190
191
  ##
@@ -9,6 +9,8 @@ require 'capybara/rspec/matcher_proxies'
9
9
  RSpec.configure do |config|
10
10
  config.include Capybara::DSL, type: :feature
11
11
  config.include Capybara::RSpecMatchers, type: :feature
12
+ config.include Capybara::DSL, type: :system
13
+ config.include Capybara::RSpecMatchers, type: :system
12
14
  config.include Capybara::RSpecMatchers, type: :view
13
15
 
14
16
  # The before and after blocks must run instantaneously, because Capybara
@@ -74,9 +74,9 @@ module Capybara
74
74
  end.join
75
75
  end
76
76
  else
77
- cls = Array(classes).group_by { |cl| cl.match?(/^!(?!!!)/) }
77
+ cls = Array(classes).reject { |c| c.is_a? Regexp }.group_by { |cl| cl.match?(/^!(?!!!)/) }
78
78
  [(cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
79
- cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join]
79
+ cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..))})" }).join]
80
80
  end
81
81
  end
82
82
  end
@@ -15,6 +15,8 @@ module Capybara
15
15
  def add_attribute_conditions(**conditions)
16
16
  @expression = conditions.inject(expression) do |xp, (name, value)|
17
17
  conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
18
+ return xp if conditions.nil?
19
+
18
20
  if xp.is_a? XPath::Expression
19
21
  xp[conditions]
20
22
  else
@@ -47,9 +49,9 @@ module Capybara
47
49
  when XPath::Expression, Regexp
48
50
  attribute_conditions(class: classes)
49
51
  else
50
- Array(classes).map do |klass|
52
+ Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
51
53
  if klass.match?(/^!(?!!!)/)
52
- !XPath.attr(:class).contains_word(klass.slice(1..-1))
54
+ !XPath.attr(:class).contains_word(klass.slice(1..))
53
55
  else
54
56
  XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
55
57
  end
@@ -15,7 +15,7 @@ module Capybara
15
15
  end
16
16
 
17
17
  def self.escape_char(char)
18
- char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06x', char.ord)
18
+ char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06<hex>x', hex: char.ord)
19
19
  end
20
20
 
21
21
  def self.split(css)
@@ -43,7 +43,7 @@ module Capybara
43
43
  when '"', "'"
44
44
  selector << parse_string(char, str)
45
45
  when '\\'
46
- selector << char + str.getc
46
+ selector << (char + str.getc)
47
47
  when ','
48
48
  selectors << selector.strip
49
49
  selector.clear
@@ -4,43 +4,65 @@ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
4
  xpath(:value, :title, :type, :name) do |locator, **options|
5
5
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
6
  btn_xpath = XPath.descendant(:button)
7
+ aria_btn_xpath = XPath.descendant[XPath.attr(:role).equals('button')]
7
8
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
8
9
 
9
10
  unless locator.nil?
10
11
  locator = locator.to_s
11
- locator_matchers = XPath.attr(:id).equals(locator) |
12
- XPath.attr(:name).equals(locator) |
13
- XPath.attr(:value).is(locator) |
14
- XPath.attr(:title).is(locator)
15
- locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
16
- locator_matchers |= XPath.attr(test_id) == locator if test_id
12
+ locator_matchers = combine_locators(locator, config: self)
13
+ btn_matchers = locator_matchers |
14
+ XPath.string.n.is(locator) |
15
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
17
16
 
17
+ label_contains_xpath = locate_label(locator).descendant[labellable_elements]
18
18
  input_btn_xpath = input_btn_xpath[locator_matchers]
19
-
20
- btn_xpath = btn_xpath[locator_matchers |
21
- XPath.string.n.is(locator) |
22
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
19
+ btn_xpath = btn_xpath[btn_matchers]
20
+ aria_btn_xpath = aria_btn_xpath[btn_matchers]
23
21
 
24
22
  alt_matches = XPath.attr(:alt).is(locator)
25
23
  alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
26
24
  image_btn_xpath = image_btn_xpath[alt_matches]
27
25
  end
28
26
 
29
- %i[value title type name].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
30
- memo[find_by_attr(ef, options[ef])]
27
+ btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath, label_contains_xpath].compact
28
+ btn_xpaths << aria_btn_xpath if enable_aria_role
29
+
30
+ %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
31
+ memo.where(find_by_attr(ef, options[ef]))
31
32
  end
32
33
  end
33
34
 
34
35
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
35
36
  expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
36
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
+
37
43
  describe_expression_filters do |disabled: nil, **options|
38
44
  desc = +''
39
45
  desc << ' that is not disabled' if disabled == false
40
- desc << describe_all_expression_filters(options)
46
+ desc << describe_all_expression_filters(**options)
41
47
  end
42
48
 
43
49
  describe_node_filters do |disabled: nil, **|
44
50
  ' that is disabled' if disabled == true
45
51
  end
52
+
53
+ def combine_locators(locator, config:)
54
+ [
55
+ XPath.attr(:id).equals(locator),
56
+ XPath.attr(:name).equals(locator),
57
+ XPath.attr(:value).is(locator),
58
+ XPath.attr(:title).is(locator),
59
+ (XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)),
60
+ (XPath.attr(:'aria-label').is(locator) if config.enable_aria_label),
61
+ (XPath.attr(config.test_id) == locator if config.test_id)
62
+ ].compact.inject(&:|)
63
+ end
64
+
65
+ def labellable_elements
66
+ (XPath.self(:input) & XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')) | XPath.self(:button)
67
+ end
46
68
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
4
4
  xpath do |locator, allow_self: nil, **options|
5
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
5
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
6
6
  XPath.attr(:type) == 'checkbox'
7
7
  ]
8
- locate_field(xpath, locator, options)
8
+ locate_field(xpath, locator, **options)
9
9
  end
10
10
 
11
11
  filter_set(:_field, %i[checked unchecked disabled name])
@@ -20,7 +20,7 @@ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
20
20
  describe_node_filters do |option: nil, with: nil, **|
21
21
  desc = +''
22
22
  desc << " with value #{option.inspect}" if option
23
- desc << " with value #{with.inspec}" if with
23
+ desc << " with value #{with.inspect}" if with
24
24
  desc
25
25
  end
26
26
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
4
  css do |css|
5
- warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead." if css.is_a? Symbol
5
+ if css.is_a? Symbol
6
+ Capybara::Helpers.warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead : #{Capybara::Helpers.filter_backtrace(caller)}"
7
+ end
6
8
  css
7
9
  end
8
10
  end
@@ -5,7 +5,7 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
5
5
 
6
6
  xpath do |locator, **options|
7
7
  xpath = XPath.descendant(:input)[XPath.attr(:list)]
8
- locate_field(xpath, locator, options)
8
+ locate_field(xpath, locator, **options)
9
9
  end
10
10
 
11
11
  filter_set(:_field, %i[disabled name placeholder])
@@ -19,7 +19,7 @@ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
19
19
 
20
20
  expression_filter(:with_options) do |expr, options|
21
21
  options.inject(expr) do |xpath, option|
22
- xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id)]
22
+ xpath.where(XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id))
23
23
  end
24
24
  end
25
25
 
@@ -16,7 +16,7 @@ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
16
16
  describe_expression_filters do |disabled: nil, **options|
17
17
  desc = +''
18
18
  desc << ' that is not disabled' if disabled == false
19
- desc << describe_all_expression_filters(options)
19
+ desc << describe_all_expression_filters(**options)
20
20
  end
21
21
 
22
22
  describe_node_filters do |**options|
@@ -18,8 +18,9 @@ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
18
18
  end
19
19
 
20
20
  describe_expression_filters do |**options|
21
- booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
22
- desc = describe_all_expression_filters(values)
21
+ boolean_values = [true, false]
22
+ booleans, values = options.partition { |_k, v| boolean_values.include? v }.map(&:to_h)
23
+ desc = describe_all_expression_filters(**values)
23
24
  desc + booleans.map do |k, v|
24
25
  v ? " with #{k} attribute" : "without #{k} attribute"
25
26
  end.join
@@ -7,7 +7,7 @@ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
7
7
  invalid_types = %w[submit image]
8
8
  invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
9
9
  xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
10
- locate_field(xpath, locator, options)
10
+ locate_field(xpath, locator, **options)
11
11
  end
12
12
 
13
13
  expression_filter(:type) do |expr, type|
@@ -3,10 +3,10 @@
3
3
  Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
4
4
  label 'file field'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
7
  XPath.attr(:type) == 'file'
8
8
  ]
9
- locate_field(xpath, locator, options)
9
+ locate_field(xpath, locator, **options)
10
10
  end
11
11
 
12
12
  filter_set(:_field, %i[disabled multiple name])
@@ -3,10 +3,10 @@
3
3
  Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
4
4
  label 'field'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input, :textarea)[
7
7
  !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
8
8
  ]
9
- locate_field(xpath, locator, options)
9
+ locate_field(xpath, locator, **options)
10
10
  end
11
11
 
12
12
  expression_filter(:type) do |expr, type|
@@ -18,7 +18,7 @@ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
18
18
  end
19
19
  end
20
20
 
21
- filter_set(:_field, %i[disabled multiple name placeholder valid])
21
+ filter_set(:_field, %i[disabled multiple name placeholder valid validation_message])
22
22
 
23
23
  node_filter(:with) do |node, with|
24
24
  val = node.value
@@ -2,7 +2,7 @@
2
2
 
3
3
  Capybara.add_selector(:label, locator_type: [String, Symbol]) do
4
4
  label 'label'
5
- xpath(:for) do |locator, options|
5
+ xpath(:for) do |locator, **options|
6
6
  xpath = XPath.descendant(:label)
7
7
  unless locator.nil?
8
8
  locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
@@ -10,7 +10,9 @@ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
10
10
  xpath = xpath[locator_matchers]
11
11
  end
12
12
  if options.key?(:for)
13
- if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
13
+ for_option = options[:for]
14
+ for_option = for_option[:id] if for_option.is_a?(Capybara::Node::Element)
15
+ if for_option && (for_option != '')
14
16
  with_attr = builder(XPath.self).add_attribute_conditions(for: for_option)
15
17
  wrapped = !XPath.attr(:for) &
16
18
  builder(XPath.self.descendant(*labelable_elements)).add_attribute_conditions(id: for_option)
@@ -51,7 +53,7 @@ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
51
53
  end
52
54
  end
53
55
  describe_node_filters do |**options|
54
- " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)
56
+ " for element #{options[:for]}" if options[:for].is_a?(Capybara::Node::Element)
55
57
  end
56
58
 
57
59
  def labelable_elements
@@ -5,6 +5,13 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
5
5
  xpath = XPath.descendant(:a)
6
6
  xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
7
 
8
+ if enable_aria_role
9
+ role_path = XPath.descendant[XPath.attr(:role).equals('link')]
10
+ role_path = builder(role_path).add_attribute_conditions(href: href) unless [true, false].include? href
11
+
12
+ xpath += role_path
13
+ end
14
+
8
15
  unless locator.nil?
9
16
  locator = locator.to_s
10
17
  matchers = [XPath.attr(:id) == locator,
@@ -18,6 +25,7 @@ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
18
25
 
19
26
  xpath = xpath[find_by_attr(:title, title)]
20
27
  xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+
21
29
  xpath
22
30
  end
23
31
 
@@ -3,10 +3,10 @@
3
3
  Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
4
4
  label 'radio button'
5
5
  xpath do |locator, allow_self: nil, **options|
6
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
7
  XPath.attr(:type) == 'radio'
8
8
  ]
9
- locate_field(xpath, locator, options)
9
+ locate_field(xpath, locator, **options)
10
10
  end
11
11
 
12
12
  filter_set(:_field, %i[checked unchecked disabled name])
@@ -21,7 +21,7 @@ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
21
21
  describe_node_filters do |option: nil, with: nil, **|
22
22
  desc = +''
23
23
  desc << " with value #{option.inspect}" if option
24
- desc << " with value #{with.inspec}" if with
24
+ desc << " with value #{with.inspect}" if with
25
25
  desc
26
26
  end
27
27
  end
@@ -5,41 +5,47 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
5
5
 
6
6
  xpath do |locator, **options|
7
7
  xpath = XPath.descendant(:select)
8
- locate_field(xpath, locator, options)
8
+ locate_field(xpath, locator, **options)
9
9
  end
10
10
 
11
11
  filter_set(:_field, %i[disabled multiple name placeholder])
12
12
 
13
13
  node_filter(:options) do |node, options|
14
- actual = if node.visible?
15
- node.all(:xpath, './/option', wait: false).map(&:text)
16
- else
17
- node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
18
- end
14
+ actual = options_text(node)
19
15
  (options.sort == actual.sort).tap do |res|
20
16
  add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
21
17
  end
22
18
  end
23
19
 
20
+ node_filter(:enabled_options) do |node, options|
21
+ actual = options_text(node) { |o| !o.disabled? }
22
+ (options.sort == actual.sort).tap do |res|
23
+ add_error("Expected enabled options #{options.inspect} found #{actual.inspect}") unless res
24
+ end
25
+ end
26
+
27
+ node_filter(:disabled_options) do |node, options|
28
+ actual = options_text(node, &:disabled?)
29
+ (options.sort == actual.sort).tap do |res|
30
+ add_error("Expected disabled options #{options.inspect} found #{actual.inspect}") unless res
31
+ end
32
+ end
33
+
24
34
  expression_filter(:with_options) do |expr, options|
25
35
  options.inject(expr) do |xpath, option|
26
- xpath[expression_for(:option, option)]
36
+ xpath.where(expression_for(:option, option))
27
37
  end
28
38
  end
29
39
 
30
40
  node_filter(:selected) do |node, selected|
31
- actual = node.all(:xpath, './/option', visible: false, wait: false)
32
- .select(&:selected?)
33
- .map { |option| option.text(:all) }
41
+ actual = options_text(node, visible: false, &:selected?)
34
42
  (Array(selected).sort == actual.sort).tap do |res|
35
43
  add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
36
44
  end
37
45
  end
38
46
 
39
47
  node_filter(:with_selected) do |node, selected|
40
- actual = node.all(:xpath, './/option', visible: false, wait: false)
41
- .select(&:selected?)
42
- .map { |option| option.text(:all) }
48
+ actual = options_text(node, visible: false, &:selected?)
43
49
  (Array(selected) - actual).empty?.tap do |res|
44
50
  add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
45
51
  end
@@ -51,12 +57,25 @@ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
51
57
  desc
52
58
  end
53
59
 
54
- describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
60
+ describe_node_filters do |
61
+ options: nil, disabled_options: nil, enabled_options: nil,
62
+ selected: nil, with_selected: nil,
63
+ disabled: nil, **|
55
64
  desc = +''
56
65
  desc << " with options #{options.inspect}" if options
66
+ desc << " with disabled options #{disabled_options.inspect}}" if disabled_options
67
+ desc << " with enabled options #{enabled_options.inspect}" if enabled_options
57
68
  desc << " with #{selected.inspect} selected" if selected
58
69
  desc << " with at least #{with_selected.inspect} selected" if with_selected
59
70
  desc << ' which is disabled' if disabled
60
71
  desc
61
72
  end
73
+
74
+ def options_text(node, **opts, &filter_block)
75
+ opts[:wait] = false
76
+ opts[:visible] = false unless node.visible?
77
+ node.all(:xpath, './/option', **opts, &filter_block).map do |o|
78
+ o.text((:all if opts[:visible] == false))
79
+ end
80
+ end
62
81
  end
@@ -19,7 +19,10 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
19
19
  header = XPath.descendant(:th)[XPath.string.n.is(header)]
20
20
  td = XPath.descendant(:tr)[header].descendant(:td)
21
21
  cell_condition = XPath.string.n.is(cell_str)
22
- cell_condition &= prev_col_position?(XPath.ancestor(:table)[1].join(xp)) if xp
22
+ if xp
23
+ prev_cell = XPath.ancestor(:table)[1].join(xp)
24
+ cell_condition &= (prev_cell & prev_col_position?(prev_cell))
25
+ end
23
26
  td[cell_condition]
24
27
  end
25
28
  else
@@ -28,7 +31,7 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
28
31
 
29
32
  if prev_cell
30
33
  prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
31
- cell_condition &= prev_col_position?(prev_cell)
34
+ cell_condition &= (prev_cell & prev_col_position?(prev_cell))
32
35
  end
33
36
 
34
37
  XPath.descendant(:td)[cell_condition]
@@ -40,7 +43,7 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
40
43
  end
41
44
 
42
45
  expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
43
- raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
46
+ raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array)
44
47
 
45
48
  rows = cols.transpose
46
49
  col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
@@ -9,12 +9,12 @@ Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
9
9
  cell_xp = XPath.descendant(:td)[
10
10
  XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
11
11
  ]
12
- xp[cell_xp]
12
+ xp.where(cell_xp)
13
13
  end
14
14
  else
15
15
  initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
16
16
  tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }
17
- .reduce { |xp, cell| xp[cell] }
17
+ .reduce { |xp, cell| xp.where(cell) }
18
18
  xpath[initial_td[tds]]
19
19
  end
20
20
  end