capybara 3.0.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +891 -12
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +29 -10
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +22 -4
  9. data/lib/capybara/driver/node.rb +38 -9
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +57 -8
  12. data/lib/capybara/minitest/spec.rb +185 -84
  13. data/lib/capybara/minitest.rb +264 -145
  14. data/lib/capybara/node/actions.rb +248 -124
  15. data/lib/capybara/node/base.rb +35 -20
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +13 -15
  18. data/lib/capybara/node/element.rb +350 -113
  19. data/lib/capybara/node/finders.rb +104 -82
  20. data/lib/capybara/node/matchers.rb +363 -157
  21. data/lib/capybara/node/simple.rb +54 -15
  22. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  23. data/lib/capybara/queries/active_element_query.rb +18 -0
  24. data/lib/capybara/queries/ancestor_query.rb +9 -10
  25. data/lib/capybara/queries/base_query.rb +25 -18
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +11 -0
  28. data/lib/capybara/queries/selector_query.rb +617 -104
  29. data/lib/capybara/queries/sibling_query.rb +9 -7
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +40 -22
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +106 -31
  34. data/lib/capybara/rack_test/driver.rb +16 -7
  35. data/lib/capybara/rack_test/errors.rb +6 -0
  36. data/lib/capybara/rack_test/form.rb +74 -49
  37. data/lib/capybara/rack_test/node.rb +120 -47
  38. data/lib/capybara/rails.rb +1 -1
  39. data/lib/capybara/registration_container.rb +41 -0
  40. data/lib/capybara/registrations/drivers.rb +42 -0
  41. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  42. data/lib/capybara/registrations/servers.rb +66 -0
  43. data/lib/capybara/result.rb +87 -53
  44. data/lib/capybara/rspec/features.rb +8 -10
  45. data/lib/capybara/rspec/matcher_proxies.rb +39 -18
  46. data/lib/capybara/rspec/matchers/base.rb +113 -0
  47. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  48. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  49. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  50. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  51. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  52. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  53. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  54. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  55. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  56. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  57. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  58. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  59. data/lib/capybara/rspec/matchers.rb +142 -315
  60. data/lib/capybara/rspec.rb +3 -2
  61. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  62. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  63. data/lib/capybara/selector/css.rb +85 -8
  64. data/lib/capybara/selector/definition/button.rb +68 -0
  65. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  66. data/lib/capybara/selector/definition/css.rb +10 -0
  67. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  68. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  69. data/lib/capybara/selector/definition/element.rb +28 -0
  70. data/lib/capybara/selector/definition/field.rb +40 -0
  71. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  72. data/lib/capybara/selector/definition/file_field.rb +13 -0
  73. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  74. data/lib/capybara/selector/definition/frame.rb +17 -0
  75. data/lib/capybara/selector/definition/id.rb +6 -0
  76. data/lib/capybara/selector/definition/label.rb +62 -0
  77. data/lib/capybara/selector/definition/link.rb +55 -0
  78. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  79. data/lib/capybara/selector/definition/option.rb +27 -0
  80. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  81. data/lib/capybara/selector/definition/select.rb +81 -0
  82. data/lib/capybara/selector/definition/table.rb +109 -0
  83. data/lib/capybara/selector/definition/table_row.rb +21 -0
  84. data/lib/capybara/selector/definition/xpath.rb +5 -0
  85. data/lib/capybara/selector/definition.rb +280 -0
  86. data/lib/capybara/selector/filter.rb +1 -0
  87. data/lib/capybara/selector/filter_set.rb +72 -28
  88. data/lib/capybara/selector/filters/base.rb +45 -2
  89. data/lib/capybara/selector/filters/expression_filter.rb +5 -6
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +18 -4
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +89 -200
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -534
  96. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  97. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  99. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  100. data/lib/capybara/selenium/driver.rb +270 -245
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  102. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  103. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  104. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  105. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  106. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  107. data/lib/capybara/selenium/extensions/find.rb +110 -0
  108. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  109. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  110. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  111. data/lib/capybara/selenium/node.rb +460 -170
  112. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  113. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  114. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  115. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  116. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  117. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  118. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  119. data/lib/capybara/selenium/patches/logs.rb +45 -0
  120. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  121. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  122. data/lib/capybara/server/animation_disabler.rb +80 -0
  123. data/lib/capybara/server/checker.rb +44 -0
  124. data/lib/capybara/server/middleware.rb +71 -0
  125. data/lib/capybara/server.rb +58 -67
  126. data/lib/capybara/session/config.rb +40 -6
  127. data/lib/capybara/session/matchers.rb +26 -19
  128. data/lib/capybara/session.rb +252 -194
  129. data/lib/capybara/spec/public/jquery.js +5 -5
  130. data/lib/capybara/spec/public/offset.js +6 -0
  131. data/lib/capybara/spec/public/test.js +126 -8
  132. data/lib/capybara/spec/session/accept_alert_spec.rb +11 -11
  133. data/lib/capybara/spec/session/accept_confirm_spec.rb +3 -3
  134. data/lib/capybara/spec/session/accept_prompt_spec.rb +9 -10
  135. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  136. data/lib/capybara/spec/session/all_spec.rb +135 -44
  137. data/lib/capybara/spec/session/ancestor_spec.rb +24 -19
  138. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +67 -38
  139. data/lib/capybara/spec/session/{assert_current_path.rb → assert_current_path_spec.rb} +20 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  141. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  142. data/lib/capybara/spec/session/{assert_text.rb → assert_text_spec.rb} +76 -52
  143. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +12 -12
  144. data/lib/capybara/spec/session/attach_file_spec.rb +126 -72
  145. data/lib/capybara/spec/session/body_spec.rb +11 -13
  146. data/lib/capybara/spec/session/check_spec.rb +112 -51
  147. data/lib/capybara/spec/session/choose_spec.rb +62 -30
  148. data/lib/capybara/spec/session/click_button_spec.rb +227 -161
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -30
  150. data/lib/capybara/spec/session/click_link_spec.rb +89 -55
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +44 -37
  153. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +3 -3
  154. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +2 -2
  155. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +8 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +70 -56
  159. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +7 -7
  160. data/lib/capybara/spec/session/evaluate_script_spec.rb +28 -8
  161. data/lib/capybara/spec/session/execute_script_spec.rb +8 -7
  162. data/lib/capybara/spec/session/fill_in_spec.rb +110 -44
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +8 -8
  165. data/lib/capybara/spec/session/find_field_spec.rb +33 -31
  166. data/lib/capybara/spec/session/find_link_spec.rb +42 -14
  167. data/lib/capybara/spec/session/find_spec.rb +251 -142
  168. data/lib/capybara/spec/session/first_spec.rb +45 -44
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +6 -6
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +6 -6
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +32 -20
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +46 -19
  173. data/lib/capybara/spec/session/go_back_spec.rb +1 -1
  174. data/lib/capybara/spec/session/go_forward_spec.rb +1 -1
  175. data/lib/capybara/spec/session/has_all_selectors_spec.rb +23 -23
  176. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  177. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  178. data/lib/capybara/spec/session/has_button_spec.rb +98 -12
  179. data/lib/capybara/spec/session/has_css_spec.rb +271 -137
  180. data/lib/capybara/spec/session/has_current_path_spec.rb +50 -35
  181. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  182. data/lib/capybara/spec/session/has_field_spec.rb +137 -58
  183. data/lib/capybara/spec/session/has_link_spec.rb +46 -6
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +33 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +117 -69
  187. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  188. data/lib/capybara/spec/session/has_table_spec.rb +181 -4
  189. data/lib/capybara/spec/session/has_text_spec.rb +101 -53
  190. data/lib/capybara/spec/session/has_title_spec.rb +19 -14
  191. data/lib/capybara/spec/session/has_xpath_spec.rb +56 -38
  192. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +1 -1
  193. data/lib/capybara/spec/session/html_spec.rb +13 -6
  194. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  195. data/lib/capybara/spec/session/node_spec.rb +958 -122
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +15 -12
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +65 -37
  199. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +1 -1
  200. data/lib/capybara/spec/session/save_and_open_page_spec.rb +2 -2
  201. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +5 -4
  202. data/lib/capybara/spec/session/save_page_spec.rb +41 -38
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +13 -11
  204. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  205. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  206. data/lib/capybara/spec/session/select_spec.rb +102 -76
  207. data/lib/capybara/spec/session/selectors_spec.rb +51 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +26 -24
  210. data/lib/capybara/spec/session/title_spec.rb +8 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +41 -22
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +79 -53
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +22 -19
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +4 -3
  216. data/lib/capybara/spec/session/window/open_new_window_spec.rb +4 -3
  217. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +25 -21
  218. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +10 -5
  219. data/lib/capybara/spec/session/window/window_spec.rb +88 -54
  220. data/lib/capybara/spec/session/window/windows_spec.rb +11 -8
  221. data/lib/capybara/spec/session/window/within_window_spec.rb +17 -16
  222. data/lib/capybara/spec/session/within_spec.rb +82 -44
  223. data/lib/capybara/spec/spec_helper.rb +46 -52
  224. data/lib/capybara/spec/test_app.rb +148 -41
  225. data/lib/capybara/spec/views/animated.erb +49 -0
  226. data/lib/capybara/spec/views/form.erb +156 -42
  227. data/lib/capybara/spec/views/frame_child.erb +4 -3
  228. data/lib/capybara/spec/views/frame_one.erb +2 -1
  229. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  230. data/lib/capybara/spec/views/frame_two.erb +1 -1
  231. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  232. data/lib/capybara/spec/views/layout.erb +10 -0
  233. data/lib/capybara/spec/views/obscured.erb +47 -0
  234. data/lib/capybara/spec/views/offset.erb +33 -0
  235. data/lib/capybara/spec/views/path.erb +2 -2
  236. data/lib/capybara/spec/views/popup_one.erb +1 -1
  237. data/lib/capybara/spec/views/popup_two.erb +1 -1
  238. data/lib/capybara/spec/views/react.erb +45 -0
  239. data/lib/capybara/spec/views/scroll.erb +21 -0
  240. data/lib/capybara/spec/views/spatial.erb +31 -0
  241. data/lib/capybara/spec/views/tables.erb +68 -1
  242. data/lib/capybara/spec/views/with_animation.erb +81 -0
  243. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  244. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  245. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  246. data/lib/capybara/spec/views/with_hover.erb +3 -2
  247. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  248. data/lib/capybara/spec/views/with_html.erb +46 -11
  249. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  250. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  251. data/lib/capybara/spec/views/with_js.erb +30 -5
  252. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  253. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  254. data/lib/capybara/spec/views/with_scope.erb +2 -2
  255. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  256. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  257. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  258. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  259. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  260. data/lib/capybara/spec/views/with_windows.erb +1 -1
  261. data/lib/capybara/spec/views/within_frames.erb +1 -1
  262. data/lib/capybara/version.rb +1 -1
  263. data/lib/capybara/window.rb +32 -26
  264. data/lib/capybara.rb +128 -104
  265. data/spec/basic_node_spec.rb +59 -34
  266. data/spec/capybara_spec.rb +65 -51
  267. data/spec/counter_spec.rb +35 -0
  268. data/spec/css_builder_spec.rb +101 -0
  269. data/spec/css_splitter_spec.rb +38 -0
  270. data/spec/dsl_spec.rb +84 -55
  271. data/spec/filter_set_spec.rb +24 -7
  272. data/spec/fixtures/certificate.pem +25 -0
  273. data/spec/fixtures/key.pem +27 -0
  274. data/spec/fixtures/selenium_driver_rspec_failure.rb +5 -5
  275. data/spec/fixtures/selenium_driver_rspec_success.rb +5 -5
  276. data/spec/minitest_spec.rb +49 -5
  277. data/spec/minitest_spec_spec.rb +92 -62
  278. data/spec/per_session_config_spec.rb +6 -6
  279. data/spec/rack_test_spec.rb +183 -115
  280. data/spec/regexp_dissassembler_spec.rb +250 -0
  281. data/spec/result_spec.rb +99 -39
  282. data/spec/rspec/features_spec.rb +28 -25
  283. data/spec/rspec/scenarios_spec.rb +10 -6
  284. data/spec/rspec/shared_spec_matchers.rb +418 -364
  285. data/spec/rspec/views_spec.rb +4 -3
  286. data/spec/rspec_matchers_spec.rb +35 -10
  287. data/spec/rspec_spec.rb +109 -85
  288. data/spec/sauce_spec_chrome.rb +43 -0
  289. data/spec/selector_spec.rb +392 -62
  290. data/spec/selenium_spec_chrome.rb +183 -41
  291. data/spec/selenium_spec_chrome_remote.rb +96 -0
  292. data/spec/selenium_spec_edge.rb +41 -8
  293. data/spec/selenium_spec_firefox.rb +228 -0
  294. data/spec/selenium_spec_firefox_remote.rb +94 -0
  295. data/spec/selenium_spec_ie.rb +129 -11
  296. data/spec/selenium_spec_safari.rb +162 -0
  297. data/spec/server_spec.rb +192 -81
  298. data/spec/session_spec.rb +52 -16
  299. data/spec/shared_selenium_node.rb +79 -0
  300. data/spec/shared_selenium_session.rb +460 -123
  301. data/spec/spec_helper.rb +124 -2
  302. data/spec/whitespace_normalizer_spec.rb +54 -0
  303. data/spec/xpath_builder_spec.rb +93 -0
  304. metadata +344 -45
  305. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  306. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  307. data/.yard/yard_extensions.rb +0 -78
  308. data/lib/capybara/rspec/compound.rb +0 -90
  309. data/lib/capybara/spec/session/assert_selector.rb +0 -149
  310. data/lib/capybara/spec/session/source_spec.rb +0 -0
  311. data/lib/capybara/spec/views/with_title.erb +0 -5
  312. data/spec/selenium_spec_marionette.rb +0 -143
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
4
+ label 'radio button'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
+ XPath.attr(:type) == 'radio'
8
+ ]
9
+ locate_field(xpath, locator, **options)
10
+ end
11
+
12
+ filter_set(:_field, %i[checked unchecked disabled name])
13
+
14
+ node_filter(%i[option with]) do |node, value|
15
+ val = node.value
16
+ (value.is_a?(Regexp) ? value.match?(val) : val == value.to_s).tap do |res|
17
+ add_error("Expected value to be #{value.inspect} but it was #{val.inspect}") unless res
18
+ end
19
+ end
20
+
21
+ describe_node_filters do |option: nil, with: nil, **|
22
+ desc = +''
23
+ desc << " with value #{option.inspect}" if option
24
+ desc << " with value #{with.inspect}" if with
25
+ desc
26
+ end
27
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
4
+ label 'select box'
5
+
6
+ xpath do |locator, **options|
7
+ xpath = XPath.descendant(:select)
8
+ locate_field(xpath, locator, **options)
9
+ end
10
+
11
+ filter_set(:_field, %i[disabled multiple name placeholder])
12
+
13
+ node_filter(:options) do |node, options|
14
+ actual = options_text(node)
15
+ (options.sort == actual.sort).tap do |res|
16
+ add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
17
+ end
18
+ end
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
+
34
+ expression_filter(:with_options) do |expr, options|
35
+ options.inject(expr) do |xpath, option|
36
+ xpath.where(expression_for(:option, option))
37
+ end
38
+ end
39
+
40
+ node_filter(:selected) do |node, selected|
41
+ actual = options_text(node, visible: false, &:selected?)
42
+ (Array(selected).sort == actual.sort).tap do |res|
43
+ add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
44
+ end
45
+ end
46
+
47
+ node_filter(:with_selected) do |node, selected|
48
+ actual = options_text(node, visible: false, &:selected?)
49
+ (Array(selected) - actual).empty?.tap do |res|
50
+ add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
51
+ end
52
+ end
53
+
54
+ describe_expression_filters do |with_options: nil, **|
55
+ desc = +''
56
+ desc << " with at least options #{with_options.inspect}" if with_options
57
+ desc
58
+ end
59
+
60
+ describe_node_filters do |
61
+ options: nil, disabled_options: nil, enabled_options: nil,
62
+ selected: nil, with_selected: nil,
63
+ disabled: nil, **|
64
+ desc = +''
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
68
+ desc << " with #{selected.inspect} selected" if selected
69
+ desc << " with at least #{with_selected.inspect} selected" if with_selected
70
+ desc << ' which is disabled' if disabled
71
+ desc
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
81
+ end
@@ -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) & header_xp.boolean & 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) & header_xp.boolean & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
11
+ ]
12
+ xp.where(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| cell.where(xp) }
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,280 @@
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 determine 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
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 should be used if no visible 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
+ key_types = %i[key keyreq]
263
+ # user filter_map when we drop dupport for 2.6
264
+ # block.parameters.select { |(type, _name)| key_types.include? type }.map { |(_, name)| name }
265
+ block.parameters.filter_map { |(type, name)| name if key_types.include? type }
266
+ end
267
+
268
+ def expression(type, allowed_filters, &block)
269
+ if block
270
+ @expressions[type] = block
271
+ allowed_filters = parameter_names(block) if allowed_filters.empty?
272
+ allowed_filters.flatten.each do |ef|
273
+ expression_filters[ef] = Capybara::Selector::Filters::IdentityExpressionFilter.new(ef)
274
+ end
275
+ end
276
+ @expressions[type]
277
+ end
278
+ end
279
+ end
280
+ 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'
@@ -5,43 +5,65 @@ require 'capybara/selector/filter'
5
5
  module Capybara
6
6
  class Selector
7
7
  class FilterSet
8
- attr_reader :descriptions
8
+ attr_reader :node_filters, :expression_filters
9
9
 
10
10
  def initialize(name, &block)
11
11
  @name = name
12
- @descriptions = []
13
- instance_eval(&block)
12
+ @node_filters = {}
13
+ @expression_filters = {}
14
+ @descriptions = Hash.new { |hsh, key| hsh[key] = [] }
15
+ instance_eval(&block) if block
14
16
  end
15
17
 
16
- def filter(name, *types_and_options, &block)
17
- 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
18
22
  end
23
+ alias_method :filter, :node_filter
19
24
 
20
- def expression_filter(name, *types_and_options, &block)
21
- 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)
22
27
  end
23
28
 
24
- def describe(&block)
25
- descriptions.push block
29
+ def describe(what = nil, &block)
30
+ case what
31
+ when nil
32
+ undeclared_descriptions.push block
33
+ when :node_filters
34
+ node_filter_descriptions.push block
35
+ when :expression_filters
36
+ expression_filter_descriptions.push block
37
+ else
38
+ raise ArgumentError, 'Unknown description type'
39
+ end
26
40
  end
27
41
 
28
- def description(**options)
42
+ def description(node_filters: true, expression_filters: true, **options)
29
43
  opts = options_with_defaults(options)
30
- @descriptions.map do |desc|
31
- desc.call(opts).to_s
32
- end.join
44
+ description = +''
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
48
+ description
33
49
  end
34
50
 
35
- def filters
36
- @filters ||= {}
51
+ def descriptions
52
+ Capybara::Helpers.warn 'DEPRECATED: FilterSet#descriptions is deprecated without replacement'
53
+ [undeclared_descriptions, node_filter_descriptions, expression_filter_descriptions].flatten
37
54
  end
38
55
 
39
- def node_filters
40
- filters.reject { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter) }.freeze
41
- end
56
+ def import(name, filters = nil)
57
+ filter_selector = filters.nil? ? ->(*) { true } : ->(filter_name, _) { filters.include? filter_name }
42
58
 
43
- def expression_filters
44
- filters.select { |_n, f| f.nil? || f.is_a?(Filters::ExpressionFilter) }.freeze
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
45
67
  end
46
68
 
47
69
  class << self
@@ -49,6 +71,10 @@ module Capybara
49
71
  @filter_sets ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
50
72
  end
51
73
 
74
+ def [](name)
75
+ all.fetch(name.to_sym) { |set_name| raise ArgumentError, "Unknown filter set (:#{set_name})" }
76
+ end
77
+
52
78
  def add(name, &block)
53
79
  all[name.to_sym] = FilterSet.new(name.to_sym, &block)
54
80
  end
@@ -58,19 +84,37 @@ module Capybara
58
84
  end
59
85
  end
60
86
 
87
+ protected
88
+
89
+ def undeclared_descriptions
90
+ @descriptions[:undeclared]
91
+ end
92
+
93
+ def node_filter_descriptions
94
+ @descriptions[:node_filters]
95
+ end
96
+
97
+ def expression_filter_descriptions
98
+ @descriptions[:expression_filters]
99
+ end
100
+
61
101
  private
62
102
 
63
103
  def options_with_defaults(options)
64
- options = options.dup
65
- filters.each do |name, filter|
66
- options[name] = filter.default if filter.default? && !options.key?(name)
67
- end
68
- options
104
+ expression_filters
105
+ .chain(node_filters)
106
+ .filter_map { |name, filter| [name, filter.default] if filter.default? }
107
+ .to_h.merge!(options)
69
108
  end
70
109
 
71
- def add_filter(name, filter_class, *types, **options, &block)
72
- types.each { |k| options[k] = true }
73
- filters[name] = filter_class.new(name, block, options)
110
+ def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
111
+ types.each { |type| options[type] = true }
112
+ if matcher && options[:default]
113
+ raise 'ArgumentError', ':default option is not supported for filters with a :matcher option'
114
+ end
115
+
116
+ filter = filter_class.new(name, matcher, block, **options)
117
+ (filter_class <= Filters::ExpressionFilter ? @expression_filters : @node_filters)[name] = filter
74
118
  end
75
119
  end
76
120
  end