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
@@ -5,13 +5,21 @@ require 'capybara/rspec/matchers/base'
5
5
  module Capybara
6
6
  module RSpecMatchers
7
7
  module Matchers
8
- class HaveSelector < WrappedElementMatcher
8
+ class HaveSelector < CountableWrappedElementMatcher
9
+ def initialize(*args, **kw_args, &filter_block)
10
+ super
11
+ if (RUBY_VERSION >= '2.7') && (@args.size < 2) && @kw_args.keys.any?(String) # rubocop:disable Style/GuardClause
12
+ @args.push(@kw_args)
13
+ @kw_args = {}
14
+ end
15
+ end
16
+
9
17
  def element_matches?(el)
10
- el.assert_selector(*@args, &@filter_block)
18
+ el.assert_selector(*@args, **session_query_options, &@filter_block)
11
19
  end
12
20
 
13
21
  def element_does_not_match?(el)
14
- el.assert_no_selector(*@args, &@filter_block)
22
+ el.assert_no_selector(*@args, **session_query_options, &@filter_block)
15
23
  end
16
24
 
17
25
  def description
@@ -19,13 +27,13 @@ module Capybara
19
27
  end
20
28
 
21
29
  def query
22
- @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, &@filter_block)
30
+ @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
31
  end
24
32
  end
25
33
 
26
34
  class HaveAllSelectors < WrappedElementMatcher
27
35
  def element_matches?(el)
28
- el.assert_all_of_selectors(*@args, &@filter_block)
36
+ el.assert_all_of_selectors(*@args, **session_query_options, &@filter_block)
29
37
  end
30
38
 
31
39
  def does_not_match?(_actual)
@@ -39,7 +47,7 @@ module Capybara
39
47
 
40
48
  class HaveNoSelectors < WrappedElementMatcher
41
49
  def element_matches?(el)
42
- el.assert_none_of_selectors(*@args, &@filter_block)
50
+ el.assert_none_of_selectors(*@args, **session_query_options, &@filter_block)
43
51
  end
44
52
 
45
53
  def does_not_match?(_actual)
@@ -53,11 +61,11 @@ module Capybara
53
61
 
54
62
  class HaveAnySelectors < WrappedElementMatcher
55
63
  def element_matches?(el)
56
- el.assert_any_of_selectors(*@args, &@filter_block)
64
+ el.assert_any_of_selectors(*@args, **session_query_options, &@filter_block)
57
65
  end
58
66
 
59
67
  def does_not_match?(_actual)
60
- el.assert_none_of_selectors(*@args, &@filter_block)
68
+ el.assert_none_of_selectors(*@args, **session_query_options, &@filter_block)
61
69
  end
62
70
 
63
71
  def description
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveSibling < CountableWrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_sibling(*@args, **session_query_options, &@filter_block)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_no_sibling(*@args, **session_query_options, &@filter_block)
15
+ end
16
+
17
+ def description
18
+ "have sibling #{query.description}"
19
+ end
20
+
21
+ def query
22
+ @query ||= Capybara::Queries::SiblingQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -5,17 +5,17 @@ require 'capybara/rspec/matchers/base'
5
5
  module Capybara
6
6
  module RSpecMatchers
7
7
  module Matchers
8
- class HaveText < WrappedElementMatcher
8
+ class HaveText < CountableWrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_text(*@args)
10
+ el.assert_text(*@args, **@kw_args)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_text(*@args)
14
+ el.assert_no_text(*@args, **@kw_args)
15
15
  end
16
16
 
17
17
  def description
18
- "text #{format(text)}"
18
+ "have text #{format(text)}"
19
19
  end
20
20
 
21
21
  def format(content)
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class HaveTitle < WrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_title(*@args)
10
+ el.assert_title(*@args, **@kw_args)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_no_title(*@args)
14
+ el.assert_no_title(*@args, **@kw_args)
15
15
  end
16
16
 
17
17
  def description
@@ -7,11 +7,11 @@ module Capybara
7
7
  module Matchers
8
8
  class MatchSelector < HaveSelector
9
9
  def element_matches?(el)
10
- el.assert_matches_selector(*@args, &@filter_block)
10
+ el.assert_matches_selector(*@args, **session_query_options, &@filter_block)
11
11
  end
12
12
 
13
13
  def element_does_not_match?(el)
14
- el.assert_not_matches_selector(*@args, &@filter_block)
14
+ el.assert_not_matches_selector(*@args, **session_query_options, &@filter_block)
15
15
  end
16
16
 
17
17
  def description
@@ -19,7 +19,7 @@ module Capybara
19
19
  end
20
20
 
21
21
  def query
22
- @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, &@filter_block)
22
+ @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, **session_query_options, &@filter_block)
23
23
  end
24
24
  end
25
25
  end
@@ -7,7 +7,7 @@ module Capybara
7
7
  module Matchers
8
8
  class MatchStyle < WrappedElementMatcher
9
9
  def element_matches?(el)
10
- el.assert_matches_style(*@args)
10
+ el.assert_matches_style(*@args, **@kw_args)
11
11
  end
12
12
 
13
13
  def does_not_match?(_actual)
@@ -28,7 +28,7 @@ module Capybara
28
28
  ##
29
29
  # @deprecated
30
30
  class HaveStyle < MatchStyle
31
- def initialize(*args, &filter_block)
31
+ def initialize(*args, **kw_args, &filter_block)
32
32
  warn 'HaveStyle matcher is deprecated, please use the MatchStyle matcher instead'
33
33
  super
34
34
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module RSpecMatchers
5
+ module SpatialSugar
6
+ def above(el)
7
+ options[:above] = el
8
+ self
9
+ end
10
+
11
+ def below(el)
12
+ options[:below] = el
13
+ self
14
+ end
15
+
16
+ def left_of(el)
17
+ options[:left_of] = el
18
+ self
19
+ end
20
+
21
+ def right_of(el)
22
+ options[:right_of] = el
23
+ self
24
+ end
25
+
26
+ def near(el)
27
+ options[:near] = el
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ def options
34
+ # (@args.last.is_a?(Hash) ? @args : @args.push({})).last
35
+ @kw_args
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,610 +2,241 @@
2
2
 
3
3
  require 'capybara/selector/xpath_extensions'
4
4
  require 'capybara/selector/selector'
5
+ require 'capybara/selector/definition'
6
+
7
+ #
8
+ # All Selectors below support the listed selector specific filters in addition to the following system-wide filters
9
+ # * :id (String, Regexp, XPath::Expression) - Matches the id attribute
10
+ # * :class (String, Array<String>, Regexp, XPath::Expression) - Matches the class(es) provided
11
+ # * :style (String, Regexp, Hash<String, String>) - Match on elements style
12
+ # * :above (Element) - Match elements above the passed element on the page
13
+ # * :below (Element) - Match elements below the passed element on the page
14
+ # * :left_of (Element) - Match elements left of the passed element on the page
15
+ # * :right_of (Element) - Match elements right of the passed element on the page
16
+ # * :near (Element) - Match elements near (within 50px) the passed element on the page
17
+ #
18
+ # ### Built-in Selectors
19
+ #
20
+ # * **:xpath** - Select elements by XPath expression
21
+ # * Locator: An XPath expression
22
+ #
23
+ # * **:css** - Select elements by CSS selector
24
+ # * Locator: A CSS selector
25
+ #
26
+ # * **:id** - Select element by id
27
+ # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
28
+ #
29
+ # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
30
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or
31
+ # associated label text
32
+ # * Filters:
33
+ # * :name (String, Regexp) - Matches the name attribute
34
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
35
+ # * :type (String) - Matches the type attribute of the field or element type for 'textarea' and 'select'
36
+ # * :readonly (Boolean) - Match on the element being readonly
37
+ # * :with (String, Regexp) - Matches the current value of the field
38
+ # * :checked (Boolean) - Match checked fields?
39
+ # * :unchecked (Boolean) - Match unchecked fields?
40
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
41
+ # * :multiple (Boolean) - Match fields that accept multiple values
42
+ # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
43
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
44
+ #
45
+ # * **:fieldset** - Select fieldset elements
46
+ # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
47
+ # * Filters:
48
+ # * :legend (String) - Matches contents of wrapped legend
49
+ # * :disabled (Boolean) - Match disabled fieldset?
50
+ #
51
+ # * **:link** - Find links (`<a>` elements with an href attribute)
52
+ # * Locator: Matches the id, {Capybara.configure test_id}, or title attributes, or the string content of the link,
53
+ # or the alt attribute of a contained img element. By default this selector requires a link to have an href attribute.
54
+ # * Filters:
55
+ # * :title (String) - Matches the title attribute
56
+ # * :alt (String) - Matches the alt attribute of a contained img element
57
+ # * :href (String, Regexp, nil, false) - Matches the normalized href of the link, if nil will find `<a>` elements with no href attribute, if false ignores href presence
58
+ #
59
+ # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
60
+ # * Locator: Matches the id, {Capybara.configure test_id} attribute, name, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
61
+ # * Filters:
62
+ # * :name (String, Regexp) - Matches the name attribute
63
+ # * :title (String) - Matches the title attribute
64
+ # * :value (String) - Matches the value of an input button
65
+ # * :type (String) - Matches the type attribute
66
+ # * :disabled (Boolean, :all) - Match disabled buttons (Default: false)
67
+ #
68
+ # * **:link_or_button** - Find links or buttons
69
+ # * Locator: See :link and :button selectors
70
+ # * Filters:
71
+ # * :disabled (Boolean, :all) - Match disabled buttons? (Default: false)
72
+ #
73
+ # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
74
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
75
+ # * Filters:
76
+ # * :name (String, Regexp) - Matches the name attribute
77
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
78
+ # * :with (String, Regexp) - Matches the current value of the field
79
+ # * :type (String) - Matches the type attribute of the field or element type for 'textarea'
80
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
81
+ # * :multiple (Boolean) - Match fields that accept multiple values
82
+ # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
83
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
84
+ #
85
+ # * **:radio_button** - Find radio buttons
86
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
87
+ # * Filters:
88
+ # * :name (String, Regexp) - Matches the name attribute
89
+ # * :checked (Boolean) - Match checked fields?
90
+ # * :unchecked (Boolean) - Match unchecked fields?
91
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
92
+ # * :option (String, Regexp) - Match the current value
93
+ # * :with - Alias of :option
94
+ #
95
+ # * **:checkbox** - Find checkboxes
96
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
97
+ # * Filters:
98
+ # * :name (String, Regexp) - Matches the name attribute
99
+ # * :checked (Boolean) - Match checked fields?
100
+ # * :unchecked (Boolean) - Match unchecked fields?
101
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
102
+ # * :with (String, Regexp) - Match the current value
103
+ # * :option - Alias of :with
104
+ #
105
+ # * **:select** - Find select elements
106
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
107
+ # * Filters:
108
+ # * :name (String, Regexp) - Matches the name attribute
109
+ # * :placeholder (String, Placeholder) - Matches the placeholder attribute
110
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
111
+ # * :multiple (Boolean) - Match fields that accept multiple values
112
+ # * :options (Array<String>) - Exact match options
113
+ # * :enabled_options (Array<String>) - Exact match enabled options
114
+ # * :disabled_options (Array<String>) - Exact match disabled options
115
+ # * :with_options (Array<String>) - Partial match options
116
+ # * :selected (String, Array<String>) - Match the selection(s)
117
+ # * :with_selected (String, Array<String>) - Partial match the selection(s)
118
+ #
119
+ # * **:option** - Find option elements
120
+ # * Locator: Match text of option
121
+ # * Filters:
122
+ # * :disabled (Boolean) - Match disabled option
123
+ # * :selected (Boolean) - Match selected option
124
+ #
125
+ # * **:datalist_input** - Find input field with datalist completion
126
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name,
127
+ # placeholder, or associated label text
128
+ # * Filters:
129
+ # * :name (String, Regexp) - Matches the name attribute
130
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
131
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
132
+ # * :options (Array<String>) - Exact match options
133
+ # * :with_options (Array<String>) - Partial match options
134
+ #
135
+ # * **:datalist_option** - Find datalist option
136
+ # * Locator: Match text or value of option
137
+ # * Filters:
138
+ # * :disabled (Boolean) - Match disabled option
139
+ #
140
+ # * **:file_field** - Find file input elements
141
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
142
+ # * Filters:
143
+ # * :name (String, Regexp) - Matches the name attribute
144
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
145
+ # * :multiple (Boolean) - Match field that accepts multiple values
146
+ #
147
+ # * **:label** - Find label elements
148
+ # * Locator: Match id, {Capybara.configure test_id}, or text contents
149
+ # * Filters:
150
+ # * :for (Element, String, Regexp) - The element or id of the element associated with the label
151
+ #
152
+ # * **:table** - Find table elements
153
+ # * Locator: id, {Capybara.configure test_id}, or caption text of table
154
+ # * Filters:
155
+ # * :caption (String) - Match text of associated caption
156
+ # * :with_rows (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
157
+ # * :rows (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
158
+ # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
159
+ # * :cols (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
160
+ #
161
+ # * **:table_row** - Find table row
162
+ # * Locator: Array<String>, Hash<String, String> table row `<td>` contents - visibility of `<td>` elements is not considered
163
+ #
164
+ # * **:frame** - Find frame/iframe elements
165
+ # * Locator: Match id, {Capybara.configure test_id} attribute, or name
166
+ # * Filters:
167
+ # * :name (String) - Match name attribute
168
+ #
169
+ # * **:element**
170
+ # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
171
+ # * Filters:
172
+ # * :\<any> (String, Regexp) - Match on any specified element attribute
173
+ #
174
+ class Capybara::Selector; end
5
175
 
6
176
  Capybara::Selector::FilterSet.add(:_field) do
7
177
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
8
178
  node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
9
179
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
10
- node_filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
11
-
12
- expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name) == val] }
13
- expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder) == val] }
14
-
15
- describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, multiple: nil, **|
16
- desc, states = +'', []
17
- states << 'checked' if checked || (unchecked == false)
18
- states << 'not checked' if unchecked || (checked == false)
19
- states << 'disabled' if disabled == true
20
- states << 'not disabled' if disabled == false
21
- desc << " that is #{states.join(' and ')}" unless states.empty?
22
- desc << ' with the multiple attribute' if multiple == true
23
- desc << ' without the multiple attribute' if multiple == false
24
- desc
25
- end
26
- end
27
-
28
- # rubocop:disable Metrics/BlockLength
29
-
30
- Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
31
- xpath { |xpath| xpath }
32
- end
33
-
34
- Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
35
- css { |css| css }
36
- end
37
-
38
- Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
39
- xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
40
- locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
41
- end
42
-
43
- Capybara.add_selector(:field, locator_type: [String, Symbol]) do
44
- visible { |options| :hidden if options[:type].to_s == 'hidden' }
45
- xpath do |locator, **options|
46
- invalid_types = %w[submit image]
47
- invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
48
- xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
49
- locate_field(xpath, locator, options)
50
- end
51
-
52
- expression_filter(:type) do |expr, type|
53
- type = type.to_s
54
- if %w[textarea select].include?(type)
55
- expr.self(type.to_sym)
56
- else
57
- expr[XPath.attr(:type) == type]
180
+ node_filter(:valid, :boolean) { |node, value| node.evaluate_script('this.validity.valid') == value }
181
+ node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
182
+ node_filter(:placeholder) { |node, value| !value.is_a?(Regexp) || value.match?(node[:placeholder]) }
183
+ node_filter(:validation_message) do |node, msg|
184
+ vm = node[:validationMessage]
185
+ (msg.is_a?(Regexp) ? msg.match?(vm) : vm == msg.to_s).tap do |res|
186
+ add_error("Expected validation message to be #{msg.inspect} but was #{vm}") unless res
58
187
  end
59
188
  end
60
189
 
61
- filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
62
-
63
- node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
64
- node_filter(:with) do |node, with|
65
- val = node.value
66
- (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
67
- add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
68
- end
190
+ expression_filter(:name) do |xpath, val|
191
+ builder(xpath).add_attribute_conditions(name: val)
69
192
  end
70
-
71
- describe_expression_filters do |type: nil, **options|
72
- desc = +''
73
- (expression_filters.keys & options.keys).each { |ef| desc << " with #{ef} #{options[ef]}" }
74
- desc << " of type #{type.inspect}" if type
75
- desc
193
+ expression_filter(:placeholder) do |xpath, val|
194
+ builder(xpath).add_attribute_conditions(placeholder: val)
76
195
  end
196
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
197
+ expression_filter(:multiple) { |xpath, val| xpath[val ? XPath.attr(:multiple) : ~XPath.attr(:multiple)] }
77
198
 
78
- describe_node_filters do |**options|
79
- " with value #{options[:with].to_s.inspect}" if options.key?(:with)
80
- end
81
- end
82
-
83
- Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
84
- xpath do |locator, legend: nil, **|
85
- locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
86
- locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
87
- xpath = XPath.descendant(:fieldset)[locator && locator_matchers]
88
- xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend
89
- xpath
90
- end
91
-
92
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
93
- end
94
-
95
- Capybara.add_selector(:link, locator_type: [String, Symbol]) do
96
- xpath do |locator, href: true, alt: nil, title: nil, **|
97
- xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
98
-
99
- unless locator.nil?
100
- locator = locator.to_s
101
- matchers = [XPath.attr(:id) == locator,
102
- XPath.string.n.is(locator),
103
- XPath.attr(:title).is(locator),
104
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
105
- matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
106
- matchers << XPath.attr(test_id).equals(locator) if test_id
107
- xpath = xpath[matchers.reduce(:|)]
108
- end
109
-
110
- xpath = xpath[find_by_attr(:title, title)]
111
- xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
112
- xpath
113
- end
114
-
115
- node_filter(:href) do |node, href|
116
- # If not a Regexp it's been handled in the main XPath
117
- (href.is_a?(Regexp) ? node[:href].match(href) : true).tap do |res|
118
- add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
119
- end
120
- end
121
-
122
- expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
123
- builder(expr).add_attribute_conditions(download: download)
124
- end
125
-
126
- describe_expression_filters do |**options|
199
+ describe(:expression_filters) do |name: nil, placeholder: nil, disabled: nil, multiple: nil, **|
127
200
  desc = +''
128
- if (href = options[:href])
129
- desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
130
- elsif options.key?(:href) # is nil/false specified?
131
- desc << ' with no href attribute'
132
- end
133
- desc
134
- end
135
- end
136
-
137
- Capybara.add_selector(:button, locator_type: [String, Symbol]) do
138
- xpath(:value, :title, :type) do |locator, **options|
139
- input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
140
- btn_xpath = XPath.descendant(:button)
141
- image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
142
-
143
- unless locator.nil?
144
- locator = locator.to_s
145
- locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
146
- locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
147
- locator_matchers |= XPath.attr(test_id) == locator if test_id
148
-
149
- input_btn_xpath = input_btn_xpath[locator_matchers]
150
-
151
- btn_xpath = btn_xpath[locator_matchers | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
152
-
153
- alt_matches = XPath.attr(:alt).is(locator)
154
- alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
155
- image_btn_xpath = image_btn_xpath[alt_matches]
156
- end
157
-
158
- %i[value title type].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
159
- memo[find_by_attr(ef, options[ef])]
160
- end
161
- end
162
-
163
- node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
164
-
165
- describe_expression_filters
166
- describe_node_filters do |disabled: nil, **|
167
- ' that is disabled' if disabled == true
168
- end
169
- end
170
-
171
- Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
172
- label 'link or button'
173
- xpath do |locator, **options|
174
- self.class.all.values_at(:link, :button).map do |selector|
175
- instance_exec(locator, options, &selector.xpath)
176
- end.reduce(:union)
177
- end
178
-
179
- node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == 'a' || !(value ^ node.disabled?) }
180
-
181
- describe_node_filters do |disabled: nil, **|
182
- ' that is disabled' if disabled == true
183
- end
184
- end
185
-
186
- Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
187
- label 'field'
188
- xpath do |locator, allow_self: nil, **options|
189
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
190
- !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
191
- ]
192
- locate_field(xpath, locator, options)
193
- end
194
-
195
- expression_filter(:type) do |expr, type|
196
- type = type.to_s
197
- if type == 'textarea'
198
- expr.self(type.to_sym)
199
- else
200
- expr[XPath.attr(:type) == type]
201
- end
202
- end
203
-
204
- filter_set(:_field, %i[disabled multiple name placeholder])
205
-
206
- node_filter(:with) do |node, with|
207
- val = node.value
208
- (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
209
- add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
210
- end
211
- end
212
-
213
- describe_expression_filters
214
- describe_node_filters do |**options|
215
- " with value #{options[:with].to_s.inspect}" if options.key?(:with)
216
- end
217
- end
218
-
219
- Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
220
- label 'radio button'
221
- xpath do |locator, allow_self: nil, **options|
222
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
223
- XPath.attr(:type) == 'radio'
224
- ]
225
- locate_field(xpath, locator, options)
226
- end
227
-
228
- filter_set(:_field, %i[checked unchecked disabled name])
229
-
230
- node_filter(:option) do |node, value|
231
- val = node.value
232
- (val == value.to_s).tap do |res|
233
- add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
234
- end
235
- end
236
-
237
- describe_expression_filters
238
- describe_node_filters do |option: nil, **|
239
- " with value #{option.inspect}" if option
240
- end
241
- end
242
-
243
- Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
244
- xpath do |locator, allow_self: nil, **options|
245
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
246
- XPath.attr(:type) == 'checkbox'
247
- ]
248
- locate_field(xpath, locator, options)
249
- end
250
-
251
- filter_set(:_field, %i[checked unchecked disabled name])
252
-
253
- node_filter(:option) do |node, value|
254
- val = node.value
255
- (val == value.to_s).tap do |res|
256
- add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
257
- end
258
- end
259
-
260
- describe_expression_filters
261
- describe_node_filters do |option: nil, **|
262
- " with value #{option.inspect}" if option
263
- end
264
- end
265
-
266
- Capybara.add_selector(:select, locator_type: [String, Symbol]) do
267
- label 'select box'
268
-
269
- xpath do |locator, **options|
270
- xpath = XPath.descendant(:select)
271
- locate_field(xpath, locator, options)
272
- end
273
-
274
- filter_set(:_field, %i[disabled multiple name placeholder])
275
-
276
- node_filter(:options) do |node, options|
277
- actual = if node.visible?
278
- node.all(:xpath, './/option', wait: false).map(&:text)
279
- else
280
- node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
281
- end
282
- (options.sort == actual.sort).tap do |res|
283
- add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
284
- end
285
- end
286
-
287
- expression_filter(:with_options) do |expr, options|
288
- options.inject(expr) do |xpath, option|
289
- xpath[self.class.all[:option].call(option)]
290
- end
291
- end
292
-
293
- node_filter(:selected) do |node, selected|
294
- actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
295
- (Array(selected).sort == actual.sort).tap do |res|
296
- add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
297
- end
298
- end
299
-
300
- node_filter(:with_selected) do |node, selected|
301
- actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
302
- (Array(selected) - actual).empty?.tap do |res|
303
- add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
304
- end
305
- end
306
-
307
- describe_expression_filters do |with_options: nil, **opts|
308
- desc = +''
309
- desc << " with at least options #{with_options.inspect}" if with_options
310
- desc << describe_all_expression_filters(opts)
311
- desc
312
- end
313
-
314
- describe_node_filters do |options: nil, selected: nil, with_selected: nil, **|
315
- desc = +''
316
- desc << " with options #{options.inspect}" if options
317
- desc << " with #{selected.inspect} selected" if selected
318
- desc << " with at least #{with_selected.inspect} selected" if with_selected
319
- desc
320
- end
321
- end
322
-
323
- Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
324
- label 'input box with datalist completion'
325
-
326
- xpath do |locator, **options|
327
- xpath = XPath.descendant(:input)[XPath.attr(:list)]
328
- locate_field(xpath, locator, options)
329
- end
330
-
331
- filter_set(:_field, %i[disabled name placeholder])
332
-
333
- node_filter(:options) do |node, options|
334
- actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
335
- (options.sort == actual.sort).tap do |res|
336
- add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
337
- end
338
- end
339
-
340
- expression_filter(:with_options) do |expr, options|
341
- options.inject(expr) do |xpath, option|
342
- xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[self.class.all[:datalist_option].call(option)].attr(:id)]
343
- end
344
- end
345
-
346
- describe_expression_filters do |with_options: nil, **opts|
347
- desc = +''
348
- desc << " with at least options #{with_options.inspect}" if with_options
349
- desc << describe_all_expression_filters(opts)
201
+ desc << ' that is not disabled' if disabled == false
202
+ desc << " with name #{name}" if name
203
+ desc << " with placeholder #{placeholder}" if placeholder
204
+ desc << ' with the multiple attribute' if multiple == true
205
+ desc << ' without the multiple attribute' if multiple == false
350
206
  desc
351
207
  end
352
208
 
353
- describe_node_filters do |options: nil, **|
354
- " with options #{options.inspect}" if options
355
- end
356
- end
357
-
358
- Capybara.add_selector(:option, locator_type: [String, Symbol]) do
359
- xpath do |locator|
360
- xpath = XPath.descendant(:option)
361
- xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
362
- xpath
363
- end
364
-
365
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
366
- node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
367
-
368
- describe_node_filters do |**options|
369
- desc = +''
370
- desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
371
- desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
209
+ describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, validation_message: nil, **|
210
+ desc, states = +'', []
211
+ states << 'checked' if checked || (unchecked == false)
212
+ states << 'not checked' if unchecked || (checked == false)
213
+ states << 'disabled' if disabled == true
214
+ desc << " that is #{states.join(' and ')}" unless states.empty?
215
+ desc << ' that is valid' if valid == true
216
+ desc << ' that is invalid' if valid == false
217
+ desc << " with validation message #{validation_message.to_s.inspect}" if validation_message
372
218
  desc
373
219
  end
374
220
  end
375
221
 
376
- Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
377
- label 'datalist option'
378
- visible(:all)
379
-
380
- xpath do |locator|
381
- xpath = XPath.descendant(:option)
382
- xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil?
383
- xpath
384
- end
385
-
386
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
387
-
388
- describe_node_filters do |**options|
389
- " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
390
- end
391
- end
392
-
393
- Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
394
- label 'file field'
395
- xpath do |locator, allow_self: nil, **options|
396
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
397
- XPath.attr(:type) == 'file'
398
- ]
399
- locate_field(xpath, locator, options)
400
- end
401
-
402
- filter_set(:_field, %i[disabled multiple name])
403
-
404
- describe_expression_filters
405
- end
406
-
407
- Capybara.add_selector(:label, locator_type: [String, Symbol]) do
408
- label 'label'
409
- xpath(:for) do |locator, options|
410
- xpath = XPath.descendant(:label)
411
- unless locator.nil?
412
- locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
413
- locator_matchers |= XPath.attr(test_id) == locator if test_id
414
- xpath = xpath[locator_matchers]
415
- end
416
- if options.key?(:for)
417
- if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
418
- with_attr = XPath.attr(:for) == for_option.to_s
419
- labelable_elements = %i[button input keygen meter output progress select textarea]
420
- wrapped = !XPath.attr(:for) &
421
- XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
422
- xpath = xpath[with_attr | wrapped]
423
- end
424
- end
425
- xpath
426
- end
427
-
428
- node_filter(:for) do |node, field_or_value|
429
- # Non element values were handled through the expression filter
430
- next true unless field_or_value.is_a? Capybara::Node::Element
431
-
432
- if (for_val = node[:for])
433
- field_or_value[:id] == for_val
434
- else
435
- field_or_value.find_xpath('./ancestor::label[1]').include? node.base
436
- end
437
- end
438
-
439
- describe_expression_filters do |**options|
440
- " for element with id of \"#{options[:for]}\"" if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
441
- end
442
- describe_node_filters do |**options|
443
- " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)
444
- end
445
- end
446
-
447
- Capybara.add_selector(:table, locator_type: [String, Symbol]) do
448
- xpath do |locator, caption: nil, **|
449
- xpath = XPath.descendant(:table)
450
- unless locator.nil?
451
- locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
452
- locator_matchers |= XPath.attr(test_id) == locator if test_id
453
- xpath = xpath[locator_matchers]
454
- end
455
- xpath = xpath[XPath.descendant(:caption) == caption] if caption
456
- xpath
457
- end
458
-
459
- expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
460
- col_conditions = cols.map do |col|
461
- if col.is_a? Hash
462
- col.reduce(nil) do |xp, (header, cell_str)|
463
- header = XPath.descendant(:th)[XPath.string.n.is(header)]
464
- td = XPath.descendant(:tr)[header].descendant(:td)
465
- cell_condition = XPath.string.n.is(cell_str)
466
- cell_condition &= prev_col_position?(XPath.ancestor(:table)[1].join(xp)) if xp
467
- td[cell_condition]
468
- end
469
- else
470
- cells_xp = col.reduce(nil) do |prev_cell, cell_str|
471
- cell_condition = XPath.string.n.is(cell_str)
472
-
473
- if prev_cell
474
- prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
475
- cell_condition &= prev_col_position?(prev_cell)
476
- end
477
-
478
- XPath.descendant(:td)[cell_condition]
479
- end
480
- XPath.descendant(:tr).join(cells_xp)
481
- end
482
- end.reduce(:&)
483
- xpath[col_conditions]
484
- end
485
-
486
- expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
487
- raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
488
-
489
- rows = cols.transpose
490
- col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
491
- xpath[match_row_count(rows.size)][col_conditions]
492
- end
493
-
494
- expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
495
- rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
496
- xpath[rows_conditions]
497
- end
498
-
499
- expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
500
- rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
501
- xpath[match_row_count(rows.size)][rows_conditions]
502
- end
503
-
504
- describe_expression_filters do |caption: nil, **|
505
- " with caption \"#{caption}\"" if caption
506
- end
507
-
508
- def prev_col_position?(cell)
509
- XPath.position.equals(cell_position(cell))
510
- end
511
-
512
- def cell_position(cell)
513
- cell.preceding_sibling(:td).count.plus(1)
514
- end
515
-
516
- def match_row(row, match_size: false)
517
- xp = XPath.descendant(:tr)[
518
- if row.is_a? Hash
519
- row_match_cells_to_headers(row)
520
- else
521
- XPath.descendant(:td)[row_match_ordered_cells(row)]
522
- end
523
- ]
524
- xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
525
- xp
526
- end
527
-
528
- def match_row_count(size)
529
- XPath.descendant(:tbody).descendant(:tr).count.equals(size) | (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
530
- end
531
-
532
- def row_match_cells_to_headers(row)
533
- row.map do |header, cell|
534
- header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
535
- XPath.descendant(:td)[
536
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
537
- ]
538
- end.reduce(:&)
539
- end
540
-
541
- def row_match_ordered_cells(row)
542
- row_conditions = row.map do |cell|
543
- XPath.self(:td)[XPath.string.n.is(cell)]
544
- end
545
- row_conditions.reverse.reduce do |cond, cell|
546
- cell[XPath.following_sibling[cond]]
547
- end
548
- end
549
- end
550
-
551
- Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
552
- xpath do |locator|
553
- xpath = XPath.descendant(:tr)
554
- if locator.is_a? Hash
555
- locator.reduce(xpath) do |xp, (header, cell)|
556
- header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
557
- cell_xp = XPath.descendant(:td)[
558
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
559
- ]
560
- xp[cell_xp]
561
- end
562
- else
563
- initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
564
- tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }.reduce { |xp, cell| xp[cell] }
565
- xpath[initial_td[tds]]
566
- end
567
- end
568
- end
569
-
570
- Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
571
- xpath do |locator, name: nil, **|
572
- xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
573
- unless locator.nil?
574
- locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)
575
- locator_matchers |= XPath.attr(test_id) == locator if test_id
576
- xpath = xpath[locator_matchers]
577
- end
578
- xpath[find_by_attr(:name, name)]
579
- end
580
-
581
- describe_expression_filters do |name: nil, **|
582
- " with name #{name}" if name
583
- end
584
- end
585
-
586
- Capybara.add_selector(:element, locator_type: [String, Symbol]) do
587
- xpath do |locator, **|
588
- XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
589
- end
590
-
591
- expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
592
- builder(xpath).add_attribute_conditions(name => val)
593
- end
594
-
595
- node_filter(:attributes, matcher: /.+/) do |node, name, val|
596
- next true unless val.is_a?(Regexp)
597
-
598
- (node[name] =~ val).tap do |res|
599
- add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
600
- end
601
- end
602
-
603
- describe_expression_filters do |**options|
604
- booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
605
- desc = describe_all_expression_filters(values)
606
- desc + booleans.map do |k, v|
607
- v ? " with #{k} attribute" : "without #{k} attribute"
608
- end.join
609
- end
610
- end
611
- # rubocop:enable Metrics/BlockLength
222
+ require 'capybara/selector/definition/xpath'
223
+ require 'capybara/selector/definition/css'
224
+ require 'capybara/selector/definition/id'
225
+ require 'capybara/selector/definition/field'
226
+ require 'capybara/selector/definition/fieldset'
227
+ require 'capybara/selector/definition/link'
228
+ require 'capybara/selector/definition/button'
229
+ require 'capybara/selector/definition/link_or_button'
230
+ require 'capybara/selector/definition/fillable_field'
231
+ require 'capybara/selector/definition/radio_button'
232
+ require 'capybara/selector/definition/checkbox'
233
+ require 'capybara/selector/definition/select'
234
+ require 'capybara/selector/definition/datalist_input'
235
+ require 'capybara/selector/definition/option'
236
+ require 'capybara/selector/definition/datalist_option'
237
+ require 'capybara/selector/definition/file_field'
238
+ require 'capybara/selector/definition/label'
239
+ require 'capybara/selector/definition/table'
240
+ require 'capybara/selector/definition/table_row'
241
+ require 'capybara/selector/definition/frame'
242
+ require 'capybara/selector/definition/element'