capybara 3.8.1 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (242) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +465 -0
  4. data/License.txt +1 -1
  5. data/README.md +58 -57
  6. data/lib/capybara/config.rb +10 -4
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +2 -2
  9. data/lib/capybara/driver/node.rb +26 -5
  10. data/lib/capybara/dsl.rb +12 -4
  11. data/lib/capybara/helpers.rb +8 -4
  12. data/lib/capybara/minitest/spec.rb +162 -85
  13. data/lib/capybara/minitest.rb +248 -148
  14. data/lib/capybara/node/actions.rb +149 -96
  15. data/lib/capybara/node/base.rb +27 -10
  16. data/lib/capybara/node/document.rb +12 -0
  17. data/lib/capybara/node/document_matchers.rb +9 -5
  18. data/lib/capybara/node/element.rb +254 -109
  19. data/lib/capybara/node/finders.rb +83 -76
  20. data/lib/capybara/node/matchers.rb +279 -141
  21. data/lib/capybara/node/simple.rb +25 -6
  22. data/lib/capybara/queries/ancestor_query.rb +5 -7
  23. data/lib/capybara/queries/base_query.rb +11 -5
  24. data/lib/capybara/queries/current_path_query.rb +3 -3
  25. data/lib/capybara/queries/match_query.rb +1 -0
  26. data/lib/capybara/queries/selector_query.rb +467 -103
  27. data/lib/capybara/queries/sibling_query.rb +5 -4
  28. data/lib/capybara/queries/style_query.rb +6 -2
  29. data/lib/capybara/queries/text_query.rb +17 -3
  30. data/lib/capybara/queries/title_query.rb +2 -2
  31. data/lib/capybara/rack_test/browser.rb +22 -15
  32. data/lib/capybara/rack_test/driver.rb +10 -1
  33. data/lib/capybara/rack_test/errors.rb +6 -0
  34. data/lib/capybara/rack_test/form.rb +33 -28
  35. data/lib/capybara/rack_test/node.rb +74 -6
  36. data/lib/capybara/registration_container.rb +44 -0
  37. data/lib/capybara/registrations/drivers.rb +36 -0
  38. data/lib/capybara/registrations/patches/puma_ssl.rb +27 -0
  39. data/lib/capybara/registrations/servers.rb +44 -0
  40. data/lib/capybara/result.rb +55 -23
  41. data/lib/capybara/rspec/features.rb +4 -4
  42. data/lib/capybara/rspec/matcher_proxies.rb +36 -15
  43. data/lib/capybara/rspec/matchers/base.rb +111 -0
  44. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  45. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  46. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  47. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  48. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  49. data/lib/capybara/rspec/matchers/have_selector.rb +77 -0
  50. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  51. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  52. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  53. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  54. data/lib/capybara/rspec/matchers/match_style.rb +38 -0
  55. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  56. data/lib/capybara/rspec/matchers.rb +117 -311
  57. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  58. data/lib/capybara/selector/builders/xpath_builder.rb +69 -0
  59. data/lib/capybara/selector/css.rb +17 -15
  60. data/lib/capybara/selector/definition/button.rb +52 -0
  61. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  62. data/lib/capybara/selector/definition/css.rb +10 -0
  63. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  64. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  65. data/lib/capybara/selector/definition/element.rb +27 -0
  66. data/lib/capybara/selector/definition/field.rb +40 -0
  67. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  68. data/lib/capybara/selector/definition/file_field.rb +13 -0
  69. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  70. data/lib/capybara/selector/definition/frame.rb +17 -0
  71. data/lib/capybara/selector/definition/id.rb +6 -0
  72. data/lib/capybara/selector/definition/label.rb +62 -0
  73. data/lib/capybara/selector/definition/link.rb +54 -0
  74. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  75. data/lib/capybara/selector/definition/option.rb +27 -0
  76. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  77. data/lib/capybara/selector/definition/select.rb +81 -0
  78. data/lib/capybara/selector/definition/table.rb +109 -0
  79. data/lib/capybara/selector/definition/table_row.rb +21 -0
  80. data/lib/capybara/selector/definition/xpath.rb +5 -0
  81. data/lib/capybara/selector/definition.rb +277 -0
  82. data/lib/capybara/selector/filter.rb +1 -0
  83. data/lib/capybara/selector/filter_set.rb +26 -19
  84. data/lib/capybara/selector/filters/base.rb +24 -5
  85. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  86. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  87. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  88. data/lib/capybara/selector/regexp_disassembler.rb +214 -0
  89. data/lib/capybara/selector/selector.rb +73 -367
  90. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  91. data/lib/capybara/selector.rb +221 -480
  92. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  93. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  94. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  95. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  96. data/lib/capybara/selenium/driver.rb +203 -86
  97. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +88 -14
  98. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +124 -0
  99. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +89 -0
  100. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  101. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  102. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  103. data/lib/capybara/selenium/extensions/find.rb +110 -0
  104. data/lib/capybara/selenium/extensions/html5_drag.rb +191 -22
  105. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  106. data/lib/capybara/selenium/extensions/scroll.rb +78 -0
  107. data/lib/capybara/selenium/logger_suppressor.rb +34 -0
  108. data/lib/capybara/selenium/node.rb +298 -93
  109. data/lib/capybara/selenium/nodes/chrome_node.rb +100 -8
  110. data/lib/capybara/selenium/nodes/edge_node.rb +104 -0
  111. data/lib/capybara/selenium/nodes/firefox_node.rb +131 -0
  112. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  113. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  114. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  115. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  116. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  117. data/lib/capybara/selenium/patches/logs.rb +45 -0
  118. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -3
  119. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  120. data/lib/capybara/server/animation_disabler.rb +4 -3
  121. data/lib/capybara/server/checker.rb +6 -2
  122. data/lib/capybara/server/middleware.rb +23 -13
  123. data/lib/capybara/server.rb +30 -7
  124. data/lib/capybara/session/config.rb +14 -10
  125. data/lib/capybara/session/matchers.rb +11 -7
  126. data/lib/capybara/session.rb +152 -111
  127. data/lib/capybara/spec/public/offset.js +6 -0
  128. data/lib/capybara/spec/public/test.js +101 -10
  129. data/lib/capybara/spec/session/all_spec.rb +96 -6
  130. data/lib/capybara/spec/session/ancestor_spec.rb +5 -0
  131. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +29 -0
  132. data/lib/capybara/spec/session/assert_current_path_spec.rb +5 -2
  133. data/lib/capybara/spec/session/assert_selector_spec.rb +0 -10
  134. data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
  135. data/lib/capybara/spec/session/assert_text_spec.rb +9 -5
  136. data/lib/capybara/spec/session/attach_file_spec.rb +63 -36
  137. data/lib/capybara/spec/session/check_spec.rb +10 -4
  138. data/lib/capybara/spec/session/choose_spec.rb +8 -2
  139. data/lib/capybara/spec/session/click_button_spec.rb +117 -61
  140. data/lib/capybara/spec/session/click_link_or_button_spec.rb +16 -0
  141. data/lib/capybara/spec/session/click_link_spec.rb +17 -6
  142. data/lib/capybara/spec/session/element/matches_selector_spec.rb +40 -39
  143. data/lib/capybara/spec/session/evaluate_script_spec.rb +13 -0
  144. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  145. data/lib/capybara/spec/session/fill_in_spec.rb +47 -6
  146. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  147. data/lib/capybara/spec/session/find_spec.rb +74 -4
  148. data/lib/capybara/spec/session/first_spec.rb +1 -1
  149. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +13 -1
  150. data/lib/capybara/spec/session/frame/within_frame_spec.rb +12 -1
  151. data/lib/capybara/spec/session/has_all_selectors_spec.rb +1 -1
  152. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  153. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  154. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  155. data/lib/capybara/spec/session/has_css_spec.rb +122 -12
  156. data/lib/capybara/spec/session/has_current_path_spec.rb +6 -4
  157. data/lib/capybara/spec/session/has_field_spec.rb +55 -0
  158. data/lib/capybara/spec/session/has_select_spec.rb +34 -6
  159. data/lib/capybara/spec/session/has_selector_spec.rb +11 -4
  160. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  161. data/lib/capybara/spec/session/has_table_spec.rb +166 -0
  162. data/lib/capybara/spec/session/has_text_spec.rb +48 -1
  163. data/lib/capybara/spec/session/has_xpath_spec.rb +17 -0
  164. data/lib/capybara/spec/session/html_spec.rb +7 -0
  165. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  166. data/lib/capybara/spec/session/node_spec.rb +643 -18
  167. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  168. data/lib/capybara/spec/session/refresh_spec.rb +4 -0
  169. data/lib/capybara/spec/session/reset_session_spec.rb +23 -8
  170. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +2 -2
  171. data/lib/capybara/spec/session/save_screenshot_spec.rb +4 -4
  172. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  173. data/lib/capybara/spec/session/select_spec.rb +10 -10
  174. data/lib/capybara/spec/session/selectors_spec.rb +36 -5
  175. data/lib/capybara/spec/session/uncheck_spec.rb +2 -2
  176. data/lib/capybara/spec/session/unselect_spec.rb +1 -1
  177. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  178. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +4 -0
  179. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -0
  180. data/lib/capybara/spec/session/window/window_spec.rb +59 -58
  181. data/lib/capybara/spec/session/window/windows_spec.rb +4 -0
  182. data/lib/capybara/spec/session/within_spec.rb +23 -0
  183. data/lib/capybara/spec/spec_helper.rb +16 -6
  184. data/lib/capybara/spec/test_app.rb +28 -23
  185. data/lib/capybara/spec/views/animated.erb +49 -0
  186. data/lib/capybara/spec/views/form.erb +48 -7
  187. data/lib/capybara/spec/views/frame_child.erb +3 -2
  188. data/lib/capybara/spec/views/frame_one.erb +1 -0
  189. data/lib/capybara/spec/views/obscured.erb +47 -0
  190. data/lib/capybara/spec/views/offset.erb +32 -0
  191. data/lib/capybara/spec/views/react.erb +45 -0
  192. data/lib/capybara/spec/views/scroll.erb +20 -0
  193. data/lib/capybara/spec/views/spatial.erb +31 -0
  194. data/lib/capybara/spec/views/tables.erb +67 -0
  195. data/lib/capybara/spec/views/with_animation.erb +29 -1
  196. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  197. data/lib/capybara/spec/views/with_hover.erb +1 -0
  198. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  199. data/lib/capybara/spec/views/with_html.erb +32 -6
  200. data/lib/capybara/spec/views/with_js.erb +3 -1
  201. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  202. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  203. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  204. data/lib/capybara/version.rb +1 -1
  205. data/lib/capybara/window.rb +11 -11
  206. data/lib/capybara.rb +118 -111
  207. data/spec/basic_node_spec.rb +14 -3
  208. data/spec/capybara_spec.rb +29 -29
  209. data/spec/css_builder_spec.rb +101 -0
  210. data/spec/dsl_spec.rb +46 -21
  211. data/spec/filter_set_spec.rb +5 -5
  212. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  213. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  214. data/spec/minitest_spec.rb +18 -4
  215. data/spec/minitest_spec_spec.rb +59 -44
  216. data/spec/rack_test_spec.rb +117 -89
  217. data/spec/regexp_dissassembler_spec.rb +250 -0
  218. data/spec/result_spec.rb +51 -49
  219. data/spec/rspec/features_spec.rb +3 -0
  220. data/spec/rspec/shared_spec_matchers.rb +112 -97
  221. data/spec/rspec_spec.rb +35 -17
  222. data/spec/sauce_spec_chrome.rb +43 -0
  223. data/spec/selector_spec.rb +244 -28
  224. data/spec/selenium_spec_chrome.rb +125 -54
  225. data/spec/selenium_spec_chrome_remote.rb +26 -12
  226. data/spec/selenium_spec_edge.rb +23 -8
  227. data/spec/selenium_spec_firefox.rb +208 -0
  228. data/spec/selenium_spec_firefox_remote.rb +15 -18
  229. data/spec/selenium_spec_ie.rb +82 -13
  230. data/spec/selenium_spec_safari.rb +148 -0
  231. data/spec/server_spec.rb +118 -77
  232. data/spec/session_spec.rb +19 -3
  233. data/spec/shared_selenium_node.rb +83 -0
  234. data/spec/shared_selenium_session.rb +110 -65
  235. data/spec/spec_helper.rb +57 -9
  236. data/spec/xpath_builder_spec.rb +93 -0
  237. metadata +257 -17
  238. data/lib/capybara/rspec/compound.rb +0 -94
  239. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -49
  240. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -121
  241. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  242. data/spec/selenium_spec_marionette.rb +0 -172
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
4
+ xpath do |locator, caption: nil, **|
5
+ xpath = XPath.descendant(:table)
6
+ unless locator.nil?
7
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
8
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
9
+ xpath = xpath[locator_matchers]
10
+ end
11
+ xpath = xpath[XPath.descendant(:caption) == caption] if caption
12
+ xpath
13
+ end
14
+
15
+ expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
16
+ col_conditions = cols.map do |col|
17
+ if col.is_a? Hash
18
+ col.reduce(nil) do |xp, (header, cell_str)|
19
+ header = XPath.descendant(:th)[XPath.string.n.is(header)]
20
+ td = XPath.descendant(:tr)[header].descendant(:td)
21
+ cell_condition = XPath.string.n.is(cell_str)
22
+ if xp
23
+ prev_cell = XPath.ancestor(:table)[1].join(xp)
24
+ cell_condition &= (prev_cell & prev_col_position?(prev_cell))
25
+ end
26
+ td[cell_condition]
27
+ end
28
+ else
29
+ cells_xp = col.reduce(nil) do |prev_cell, cell_str|
30
+ cell_condition = XPath.string.n.is(cell_str)
31
+
32
+ if prev_cell
33
+ prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
34
+ cell_condition &= (prev_cell & prev_col_position?(prev_cell))
35
+ end
36
+
37
+ XPath.descendant(:td)[cell_condition]
38
+ end
39
+ XPath.descendant(:tr).join(cells_xp)
40
+ end
41
+ end.reduce(:&)
42
+ xpath[col_conditions]
43
+ end
44
+
45
+ expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
46
+ raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array)
47
+
48
+ rows = cols.transpose
49
+ col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
50
+ xpath[match_row_count(rows.size)][col_conditions]
51
+ end
52
+
53
+ expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
54
+ rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
55
+ xpath[rows_conditions]
56
+ end
57
+
58
+ expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
59
+ rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
60
+ xpath[match_row_count(rows.size)][rows_conditions]
61
+ end
62
+
63
+ describe_expression_filters do |caption: nil, **|
64
+ " with caption \"#{caption}\"" if caption
65
+ end
66
+
67
+ def prev_col_position?(cell)
68
+ XPath.position.equals(cell_position(cell))
69
+ end
70
+
71
+ def cell_position(cell)
72
+ cell.preceding_sibling(:td).count.plus(1)
73
+ end
74
+
75
+ def match_row(row, match_size: false)
76
+ xp = XPath.descendant(:tr)[
77
+ if row.is_a? Hash
78
+ row_match_cells_to_headers(row)
79
+ else
80
+ XPath.descendant(:td)[row_match_ordered_cells(row)]
81
+ end
82
+ ]
83
+ xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
84
+ xp
85
+ end
86
+
87
+ def match_row_count(size)
88
+ XPath.descendant(:tbody).descendant(:tr).count.equals(size) |
89
+ (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
90
+ end
91
+
92
+ def row_match_cells_to_headers(row)
93
+ row.map do |header, cell|
94
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
95
+ XPath.descendant(:td)[
96
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
97
+ ]
98
+ end.reduce(:&)
99
+ end
100
+
101
+ def row_match_ordered_cells(row)
102
+ row_conditions = row.map do |cell|
103
+ XPath.self(:td)[XPath.string.n.is(cell)]
104
+ end
105
+ row_conditions.reverse.reduce do |cond, cell|
106
+ cell[XPath.following_sibling[cond]]
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
4
+ xpath do |locator|
5
+ xpath = XPath.descendant(:tr)
6
+ if locator.is_a? Hash
7
+ locator.reduce(xpath) do |xp, (header, cell)|
8
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
9
+ cell_xp = XPath.descendant(:td)[
10
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
11
+ ]
12
+ xp[cell_xp]
13
+ end
14
+ else
15
+ initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
16
+ tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }
17
+ .reduce { |xp, cell| xp[cell] }
18
+ xpath[initial_td[tds]]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
4
+ xpath { |xpath| xpath }
5
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/filter_set'
4
+ require 'capybara/selector/css'
5
+ require 'capybara/selector/regexp_disassembler'
6
+ require 'capybara/selector/builders/xpath_builder'
7
+ require 'capybara/selector/builders/css_builder'
8
+
9
+ module Capybara
10
+ class Selector
11
+ class Definition
12
+ attr_reader :name, :expressions
13
+
14
+ extend Forwardable
15
+
16
+ def initialize(name, locator_type: nil, raw_locator: false, supports_exact: nil, &block)
17
+ @name = name
18
+ @filter_set = Capybara::Selector::FilterSet.add(name) {}
19
+ @match = nil
20
+ @label = nil
21
+ @failure_message = nil
22
+ @expressions = {}
23
+ @expression_filters = {}
24
+ @locator_filter = nil
25
+ @default_visibility = nil
26
+ @locator_type = locator_type
27
+ @raw_locator = raw_locator
28
+ @supports_exact = supports_exact
29
+ instance_eval(&block)
30
+ end
31
+
32
+ def custom_filters
33
+ warn "Deprecated: Selector#custom_filters is not valid when same named expression and node filter exist - don't use"
34
+ node_filters.merge(expression_filters).freeze
35
+ end
36
+
37
+ def node_filters
38
+ @filter_set.node_filters
39
+ end
40
+
41
+ def expression_filters
42
+ @filter_set.expression_filters
43
+ end
44
+
45
+ ##
46
+ #
47
+ # Define a selector by an xpath expression
48
+ #
49
+ # @overload xpath(*expression_filters, &block)
50
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that are implemented via this expression, if not specified the names of any keyword parameters in the block will be used
51
+ # @yield [locator, options] The block to use to generate the XPath expression
52
+ # @yieldparam [String] locator The locator string passed to the query
53
+ # @yieldparam [Hash] options The options hash passed to the query
54
+ # @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
55
+ #
56
+ # @overload xpath()
57
+ # @return [#call] The block that will be called to generate the XPath expression
58
+ #
59
+ def xpath(*allowed_filters, &block)
60
+ expression(:xpath, allowed_filters, &block)
61
+ end
62
+
63
+ ##
64
+ #
65
+ # Define a selector by a CSS selector
66
+ #
67
+ # @overload css(*expression_filters, &block)
68
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
69
+ # @yield [locator, options] The block to use to generate the CSS selector
70
+ # @yieldparam [String] locator The locator string passed to the query
71
+ # @yieldparam [Hash] options The options hash passed to the query
72
+ # @yieldreturn [#to_s] An object that can produce a CSS selector
73
+ #
74
+ # @overload css()
75
+ # @return [#call] The block that will be called to generate the CSS selector
76
+ #
77
+ def css(*allowed_filters, &block)
78
+ expression(:css, allowed_filters, &block)
79
+ end
80
+
81
+ ##
82
+ #
83
+ # Automatic selector detection
84
+ #
85
+ # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
86
+ # @yieldparam [String], locator The locator string used to determin if it matches the selector
87
+ # @yieldreturn [Boolean] Whether this selector matches the locator string
88
+ # @return [#call] The block that will be used to detect selector match
89
+ #
90
+ def match(&block)
91
+ @match = block if block
92
+ @match
93
+ end
94
+
95
+ ##
96
+ #
97
+ # Set/get a descriptive label for the selector
98
+ #
99
+ # @overload label(label)
100
+ # @param [String] label A descriptive label for this selector - used in error messages
101
+ # @overload label()
102
+ # @return [String] The currently set label
103
+ #
104
+ def label(label = nil)
105
+ @label = label if label
106
+ @label
107
+ end
108
+
109
+ ##
110
+ #
111
+ # Description of the selector
112
+ #
113
+ # @!method description(options)
114
+ # @param [Hash] options The options of the query used to generate the description
115
+ # @return [String] Description of the selector when used with the options passed
116
+ def_delegator :@filter_set, :description
117
+
118
+ ##
119
+ #
120
+ # Should this selector be used for the passed in locator
121
+ #
122
+ # This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
123
+ #
124
+ # @param [String] locator The locator passed to the query
125
+ # @return [Boolean] Whether or not to use this selector
126
+ #
127
+ def match?(locator)
128
+ @match&.call(locator)
129
+ end
130
+
131
+ ##
132
+ #
133
+ # Define a node filter for use with this selector
134
+ #
135
+ # @!method node_filter(name, *types, options={}, &block)
136
+ # @param [Symbol, Regexp] name The filter name
137
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
138
+ # @param [Hash] options ({}) Options of the filter
139
+ # @option options [Array<>] :valid_values Valid values for this filter
140
+ # @option options :default The default value of the filter (if any)
141
+ # @option options :skip_if Value of the filter that will cause it to be skipped
142
+ # @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
143
+ #
144
+ # If a Symbol is passed for the name the block should accept | node, option_value |, while if a Regexp
145
+ # is passed for the name the block should accept | node, option_name, option_value |. In either case
146
+ # the block should return `true` if the node passes the filer or `false` if it doesn't
147
+
148
+ ##
149
+ #
150
+ # Define an expression filter for use with this selector
151
+ #
152
+ # @!method expression_filter(name, *types, matcher: nil, **options, &block)
153
+ # @param [Symbol, Regexp] name The filter name
154
+ # @param [Regexp] matcher (nil) A Regexp used to check whether a specific option is handled by this filter
155
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
156
+ # @param [Hash] options ({}) Options of the filter
157
+ # @option options [Array<>] :valid_values Valid values for this filter
158
+ # @option options :default The default value of the filter (if any)
159
+ # @option options :skip_if Value of the filter that will cause it to be skipped
160
+ # @option options [Regexp] :matcher (nil) A Regexp used to check whether a specific option is handled by this filter. If not provided the filter will be used for options matching the filter name.
161
+ #
162
+ # If a Symbol is passed for the name the block should accept | current_expression, option_value |, while if a Regexp
163
+ # is passed for the name the block should accept | current_expression, option_name, option_value |. In either case
164
+ # the block should return the modified expression
165
+
166
+ def_delegators :@filter_set, :node_filter, :expression_filter, :filter
167
+
168
+ def locator_filter(*types, **options, &block)
169
+ types.each { |type| options[type] = true }
170
+ @locator_filter = Capybara::Selector::Filters::LocatorFilter.new(block, **options) if block
171
+ @locator_filter
172
+ end
173
+
174
+ def filter_set(name, filters_to_use = nil)
175
+ @filter_set.import(name, filters_to_use)
176
+ end
177
+
178
+ def_delegator :@filter_set, :describe
179
+
180
+ def describe_expression_filters(&block)
181
+ if block_given?
182
+ describe(:expression_filters, &block)
183
+ else
184
+ describe(:expression_filters) do |**options|
185
+ describe_all_expression_filters(**options)
186
+ end
187
+ end
188
+ end
189
+
190
+ def describe_all_expression_filters(**opts)
191
+ expression_filters.map do |ef_name, ef|
192
+ if ef.matcher?
193
+ handled_custom_options(ef, opts).map { |option, value| " with #{ef_name}[#{option} => #{value}]" }.join
194
+ elsif opts.key?(ef_name)
195
+ " with #{ef_name} #{opts[ef_name]}"
196
+ end
197
+ end.join
198
+ end
199
+
200
+ def describe_node_filters(&block)
201
+ describe(:node_filters, &block)
202
+ end
203
+
204
+ ##
205
+ #
206
+ # Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
207
+ # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
208
+ #
209
+ # @param [Symbol] default_visibility Only find elements with the specified visibility:
210
+ # * :all - finds visible and invisible elements.
211
+ # * :hidden - only finds invisible elements.
212
+ # * :visible - only finds visible elements.
213
+ def visible(default_visibility = nil, &block)
214
+ @default_visibility = block || default_visibility
215
+ end
216
+
217
+ def default_visibility(fallback = Capybara.ignore_hidden_elements, options = {})
218
+ vis = if @default_visibility&.respond_to?(:call)
219
+ @default_visibility.call(options)
220
+ else
221
+ @default_visibility
222
+ end
223
+ vis.nil? ? fallback : vis
224
+ end
225
+
226
+ # @api private
227
+ def raw_locator?
228
+ !!@raw_locator
229
+ end
230
+
231
+ # @api private
232
+ def supports_exact?
233
+ @supports_exact
234
+ end
235
+
236
+ def default_format
237
+ return nil if @expressions.keys.empty?
238
+
239
+ if @expressions.size == 1
240
+ @expressions.keys.first
241
+ else
242
+ :xpath
243
+ end
244
+ end
245
+
246
+ # @api private
247
+ def locator_types
248
+ return nil unless @locator_type
249
+
250
+ Array(@locator_type)
251
+ end
252
+
253
+ private
254
+
255
+ def handled_custom_options(filter, options)
256
+ options.select do |option, _|
257
+ filter.handles_option?(option) && !::Capybara::Queries::SelectorQuery::VALID_KEYS.include?(option)
258
+ end
259
+ end
260
+
261
+ def parameter_names(block)
262
+ block.parameters.select { |(type, _name)| %i[key keyreq].include? type }.map { |(_type, name)| name }
263
+ end
264
+
265
+ def expression(type, allowed_filters, &block)
266
+ if block
267
+ @expressions[type] = block
268
+ allowed_filters = parameter_names(block) if allowed_filters.empty?
269
+ allowed_filters.flatten.each do |ef|
270
+ expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
271
+ end
272
+ end
273
+ @expressions[type]
274
+ end
275
+ end
276
+ end
277
+ end
@@ -2,3 +2,4 @@
2
2
 
3
3
  require 'capybara/selector/filters/node_filter'
4
4
  require 'capybara/selector/filters/expression_filter'
5
+ require 'capybara/selector/filters/locator_filter'
@@ -15,13 +15,15 @@ module Capybara
15
15
  instance_eval(&block)
16
16
  end
17
17
 
18
- def node_filter(name, *types_and_options, &block)
19
- add_filter(name, Filters::NodeFilter, *types_and_options, &block)
18
+ def node_filter(names, *types, **options, &block)
19
+ Array(names).each do |name|
20
+ add_filter(name, Filters::NodeFilter, *types, **options, &block)
21
+ end
20
22
  end
21
23
  alias_method :filter, :node_filter
22
24
 
23
- def expression_filter(name, *types_and_options, &block)
24
- add_filter(name, Filters::ExpressionFilter, *types_and_options, &block)
25
+ def expression_filter(name, *types, **options, &block)
26
+ add_filter(name, Filters::ExpressionFilter, *types, **options, &block)
25
27
  end
26
28
 
27
29
  def describe(what = nil, &block)
@@ -40,9 +42,9 @@ module Capybara
40
42
  def description(node_filters: true, expression_filters: true, **options)
41
43
  opts = options_with_defaults(options)
42
44
  description = +''
43
- description += undeclared_descriptions.map { |desc| desc.call(opts).to_s }.join
44
- description += expression_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if expression_filters
45
- description += node_filter_descriptions.map { |desc| desc.call(opts).to_s }.join if node_filters
45
+ description << undeclared_descriptions.map { |desc| desc.call(**opts).to_s }.join
46
+ description << expression_filter_descriptions.map { |desc| desc.call(**opts).to_s }.join if expression_filters
47
+ description << node_filter_descriptions.map { |desc| desc.call(**opts).to_s }.join if node_filters
46
48
  description
47
49
  end
48
50
 
@@ -52,15 +54,16 @@ module Capybara
52
54
  end
53
55
 
54
56
  def import(name, filters = nil)
55
- f_set = self.class.all[name]
56
57
  filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }
57
58
 
58
- expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
59
- node_filters.merge!(f_set.node_filters.select(&filter_selector))
60
-
61
- f_set.undeclared_descriptions.each { |desc| describe(&desc) }
62
- f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
63
- f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
59
+ self.class[name].tap do |f_set|
60
+ expression_filters.merge!(f_set.expression_filters.select(&filter_selector))
61
+ node_filters.merge!(f_set.node_filters.select(&filter_selector))
62
+ f_set.undeclared_descriptions.each { |desc| describe(&desc) }
63
+ f_set.expression_filter_descriptions.each { |desc| describe(:expression_filters, &desc) }
64
+ f_set.node_filter_descriptions.each { |desc| describe(:node_filters, &desc) }
65
+ end
66
+ self
64
67
  end
65
68
 
66
69
  class << self
@@ -68,6 +71,10 @@ module Capybara
68
71
  @filter_sets ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
69
72
  end
70
73
 
74
+ def [](name)
75
+ all.fetch(name.to_sym) { |set_name| raise ArgumentError, "Unknown filter set (:#{set_name})" }
76
+ end
77
+
71
78
  def add(name, &block)
72
79
  all[name.to_sym] = FilterSet.new(name.to_sym, &block)
73
80
  end
@@ -105,12 +112,12 @@ module Capybara
105
112
 
106
113
  def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
107
114
  types.each { |type| options[type] = true }
108
- raise 'ArgumentError', ':default option is not supported for filters with a :matcher option' if matcher && options[:default]
109
- if filter_class <= Filters::ExpressionFilter
110
- @expression_filters[name] = filter_class.new(name, matcher, block, options)
111
- else
112
- @node_filters[name] = filter_class.new(name, matcher, block, options)
115
+ if matcher && options[:default]
116
+ raise 'ArgumentError', ':default option is not supported for filters with a :matcher option'
113
117
  end
118
+
119
+ filter = filter_class.new(name, matcher, block, **options)
120
+ (filter_class <= Filters::ExpressionFilter ? @expression_filters : @node_filters)[name] = filter
114
121
  end
115
122
  end
116
123
  end
@@ -24,13 +24,21 @@ module Capybara
24
24
  @options.key?(:skip_if) && value == @options[:skip_if]
25
25
  end
26
26
 
27
+ def format
28
+ @options[:format]
29
+ end
30
+
27
31
  def matcher?
28
32
  !@matcher.nil?
29
33
  end
30
34
 
35
+ def boolean?
36
+ !!@options[:boolean]
37
+ end
38
+
31
39
  def handles_option?(option_name)
32
40
  if matcher?
33
- option_name =~ @matcher
41
+ @matcher.match? option_name
34
42
  else
35
43
  @name == option_name
36
44
  end
@@ -38,18 +46,29 @@ module Capybara
38
46
 
39
47
  private
40
48
 
41
- def apply(subject, name, value, skip_value)
49
+ def apply(subject, name, value, skip_value, ctx)
42
50
  return skip_value if skip?(value)
43
- raise ArgumentError, "Invalid value #{value.inspect} passed to #{self.class.name.split('::').last} #{name}#{" : #{@name}" if @name.is_a?(Regexp)}" unless valid_value?(value)
51
+
52
+ unless valid_value?(value)
53
+ raise ArgumentError,
54
+ "Invalid value #{value.inspect} passed to #{self.class.name.split('::').last} #{name}" \
55
+ "#{" : #{name}" if @name.is_a?(Regexp)}"
56
+ end
57
+
44
58
  if @block.arity == 2
45
- @block.call(subject, value)
59
+ filter_context(ctx).instance_exec(subject, value, &@block)
46
60
  else
47
- @block.call(subject, name, value)
61
+ filter_context(ctx).instance_exec(subject, name, value, &@block)
48
62
  end
49
63
  end
50
64
 
65
+ def filter_context(context)
66
+ context || @block.binding.receiver
67
+ end
68
+
51
69
  def valid_value?(value)
52
70
  return true unless @options.key?(:valid_values)
71
+
53
72
  Array(@options[:valid_values]).any? { |valid| valid === value } # rubocop:disable Style/CaseEquality
54
73
  end
55
74
  end
@@ -6,8 +6,8 @@ module Capybara
6
6
  class Selector
7
7
  module Filters
8
8
  class ExpressionFilter < Base
9
- def apply_filter(expr, name, value)
10
- apply(expr, name, value, expr)
9
+ def apply_filter(expr, name, value, selector)
10
+ apply(expr, name, value, expr, selector)
11
11
  end
12
12
  end
13
13
 
@@ -15,7 +15,7 @@ module Capybara
15
15
  def initialize(name); super(name, nil, nil); end
16
16
  def default?; false; end
17
17
  def matcher?; false; end
18
- def apply_filter(expr, _name, _value); expr; end
18
+ def apply_filter(expr, _name, _value, _ctx); expr; end
19
19
  end
20
20
  end
21
21
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/filters/base'
4
+
5
+ module Capybara
6
+ class Selector
7
+ module Filters
8
+ class LocatorFilter < NodeFilter
9
+ def initialize(block, **options)
10
+ super(nil, nil, block, **options)
11
+ end
12
+
13
+ def matches?(node, value, context = nil, exact:)
14
+ apply(node, value, true, context, exact: exact, format: context&.default_format)
15
+ rescue Capybara::ElementNotFound
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def apply(subject, value, skip_value, ctx, **options)
22
+ return skip_value if skip?(value)
23
+
24
+ filter_context(ctx).instance_exec(subject, value, **options, &@block)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -6,8 +6,22 @@ module Capybara
6
6
  class Selector
7
7
  module Filters
8
8
  class NodeFilter < Base
9
- def matches?(node, name, value)
10
- apply(node, name, value, true)
9
+ def initialize(name, matcher, block, **options)
10
+ super
11
+ @block = if boolean?
12
+ proc do |node, value|
13
+ error_cnt = errors.size
14
+ block.call(node, value).tap do |res|
15
+ add_error("Expected #{name} #{value} but it wasn't") if !res && error_cnt == errors.size
16
+ end
17
+ end
18
+ else
19
+ block
20
+ end
21
+ end
22
+
23
+ def matches?(node, name, value, context = nil)
24
+ apply(node, name, value, true, context)
11
25
  rescue Capybara::ElementNotFound
12
26
  false
13
27
  end