capybara 2.18.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 (316) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/History.md +945 -12
  4. data/License.txt +1 -1
  5. data/README.md +264 -90
  6. data/lib/capybara/config.rb +29 -57
  7. data/lib/capybara/cucumber.rb +2 -3
  8. data/lib/capybara/driver/base.rb +35 -18
  9. data/lib/capybara/driver/node.rb +40 -10
  10. data/lib/capybara/dsl.rb +10 -7
  11. data/lib/capybara/helpers.rb +70 -31
  12. data/lib/capybara/minitest/spec.rb +185 -83
  13. data/lib/capybara/minitest.rb +232 -112
  14. data/lib/capybara/node/actions.rb +274 -171
  15. data/lib/capybara/node/base.rb +42 -34
  16. data/lib/capybara/node/document.rb +15 -3
  17. data/lib/capybara/node/document_matchers.rb +19 -21
  18. data/lib/capybara/node/element.rb +362 -135
  19. data/lib/capybara/node/finders.rb +151 -137
  20. data/lib/capybara/node/matchers.rb +394 -209
  21. data/lib/capybara/node/simple.rb +59 -26
  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 +12 -9
  25. data/lib/capybara/queries/base_query.rb +39 -28
  26. data/lib/capybara/queries/current_path_query.rb +21 -27
  27. data/lib/capybara/queries/match_query.rb +14 -7
  28. data/lib/capybara/queries/selector_query.rb +659 -149
  29. data/lib/capybara/queries/sibling_query.rb +11 -9
  30. data/lib/capybara/queries/style_query.rb +45 -0
  31. data/lib/capybara/queries/text_query.rb +56 -38
  32. data/lib/capybara/queries/title_query.rb +8 -11
  33. data/lib/capybara/rack_test/browser.rb +114 -42
  34. data/lib/capybara/rack_test/css_handlers.rb +6 -4
  35. data/lib/capybara/rack_test/driver.rb +22 -17
  36. data/lib/capybara/rack_test/errors.rb +6 -0
  37. data/lib/capybara/rack_test/form.rb +93 -58
  38. data/lib/capybara/rack_test/node.rb +184 -81
  39. data/lib/capybara/rails.rb +3 -7
  40. data/lib/capybara/registration_container.rb +41 -0
  41. data/lib/capybara/registrations/drivers.rb +42 -0
  42. data/lib/capybara/registrations/patches/puma_ssl.rb +29 -0
  43. data/lib/capybara/registrations/servers.rb +66 -0
  44. data/lib/capybara/result.rb +97 -63
  45. data/lib/capybara/rspec/features.rb +17 -50
  46. data/lib/capybara/rspec/matcher_proxies.rb +52 -15
  47. data/lib/capybara/rspec/matchers/base.rb +113 -0
  48. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  49. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  50. data/lib/capybara/rspec/matchers/count_sugar.rb +37 -0
  51. data/lib/capybara/rspec/matchers/have_ancestor.rb +28 -0
  52. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  53. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  54. data/lib/capybara/rspec/matchers/have_sibling.rb +27 -0
  55. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  56. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  57. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  58. data/lib/capybara/rspec/matchers/match_style.rb +43 -0
  59. data/lib/capybara/rspec/matchers/spatial_sugar.rb +39 -0
  60. data/lib/capybara/rspec/matchers.rb +146 -310
  61. data/lib/capybara/rspec.rb +7 -11
  62. data/lib/capybara/selector/builders/css_builder.rb +84 -0
  63. data/lib/capybara/selector/builders/xpath_builder.rb +71 -0
  64. data/lib/capybara/selector/css.rb +85 -13
  65. data/lib/capybara/selector/definition/button.rb +68 -0
  66. data/lib/capybara/selector/definition/checkbox.rb +26 -0
  67. data/lib/capybara/selector/definition/css.rb +10 -0
  68. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  69. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  70. data/lib/capybara/selector/definition/element.rb +28 -0
  71. data/lib/capybara/selector/definition/field.rb +40 -0
  72. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  73. data/lib/capybara/selector/definition/file_field.rb +13 -0
  74. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  75. data/lib/capybara/selector/definition/frame.rb +17 -0
  76. data/lib/capybara/selector/definition/id.rb +6 -0
  77. data/lib/capybara/selector/definition/label.rb +62 -0
  78. data/lib/capybara/selector/definition/link.rb +55 -0
  79. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  80. data/lib/capybara/selector/definition/option.rb +27 -0
  81. data/lib/capybara/selector/definition/radio_button.rb +27 -0
  82. data/lib/capybara/selector/definition/select.rb +81 -0
  83. data/lib/capybara/selector/definition/table.rb +109 -0
  84. data/lib/capybara/selector/definition/table_row.rb +21 -0
  85. data/lib/capybara/selector/definition/xpath.rb +5 -0
  86. data/lib/capybara/selector/definition.rb +280 -0
  87. data/lib/capybara/selector/filter.rb +2 -17
  88. data/lib/capybara/selector/filter_set.rb +80 -33
  89. data/lib/capybara/selector/filters/base.rb +50 -6
  90. data/lib/capybara/selector/filters/expression_filter.rb +8 -26
  91. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  92. data/lib/capybara/selector/filters/node_filter.rb +16 -12
  93. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  94. data/lib/capybara/selector/selector.rb +93 -210
  95. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  96. data/lib/capybara/selector.rb +475 -523
  97. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -0
  98. data/lib/capybara/selenium/atoms/isDisplayed.min.js +1 -0
  99. data/lib/capybara/selenium/atoms/src/getAttribute.js +161 -0
  100. data/lib/capybara/selenium/atoms/src/isDisplayed.js +454 -0
  101. data/lib/capybara/selenium/driver.rb +298 -267
  102. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +117 -0
  103. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +128 -0
  104. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +84 -0
  105. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +26 -0
  106. data/lib/capybara/selenium/driver_specializations/safari_driver.rb +24 -0
  107. data/lib/capybara/selenium/extensions/file_input_click_emulation.rb +34 -0
  108. data/lib/capybara/selenium/extensions/find.rb +110 -0
  109. data/lib/capybara/selenium/extensions/html5_drag.rb +229 -0
  110. data/lib/capybara/selenium/extensions/modifier_keys_stack.rb +28 -0
  111. data/lib/capybara/selenium/extensions/scroll.rb +76 -0
  112. data/lib/capybara/selenium/node.rb +517 -145
  113. data/lib/capybara/selenium/nodes/chrome_node.rb +125 -0
  114. data/lib/capybara/selenium/nodes/edge_node.rb +110 -0
  115. data/lib/capybara/selenium/nodes/firefox_node.rb +136 -0
  116. data/lib/capybara/selenium/nodes/ie_node.rb +22 -0
  117. data/lib/capybara/selenium/nodes/safari_node.rb +118 -0
  118. data/lib/capybara/selenium/patches/atoms.rb +18 -0
  119. data/lib/capybara/selenium/patches/is_displayed.rb +16 -0
  120. data/lib/capybara/selenium/patches/logs.rb +45 -0
  121. data/lib/capybara/selenium/patches/pause_duration_fix.rb +9 -0
  122. data/lib/capybara/selenium/patches/persistent_client.rb +20 -0
  123. data/lib/capybara/server/animation_disabler.rb +80 -0
  124. data/lib/capybara/server/checker.rb +44 -0
  125. data/lib/capybara/server/middleware.rb +71 -0
  126. data/lib/capybara/server.rb +59 -67
  127. data/lib/capybara/session/config.rb +81 -67
  128. data/lib/capybara/session/matchers.rb +28 -20
  129. data/lib/capybara/session.rb +337 -365
  130. data/lib/capybara/spec/public/jquery.js +5 -5
  131. data/lib/capybara/spec/public/offset.js +6 -0
  132. data/lib/capybara/spec/public/test.js +151 -12
  133. data/lib/capybara/spec/session/accept_alert_spec.rb +12 -11
  134. data/lib/capybara/spec/session/accept_confirm_spec.rb +6 -5
  135. data/lib/capybara/spec/session/accept_prompt_spec.rb +10 -10
  136. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  137. data/lib/capybara/spec/session/all_spec.rb +161 -57
  138. data/lib/capybara/spec/session/ancestor_spec.rb +27 -24
  139. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +68 -38
  140. data/lib/capybara/spec/session/assert_current_path_spec.rb +75 -0
  141. data/lib/capybara/spec/session/assert_selector_spec.rb +143 -0
  142. data/lib/capybara/spec/session/assert_style_spec.rb +26 -0
  143. data/lib/capybara/spec/session/assert_text_spec.rb +258 -0
  144. data/lib/capybara/spec/session/{assert_title.rb → assert_title_spec.rb} +22 -12
  145. data/lib/capybara/spec/session/attach_file_spec.rb +144 -69
  146. data/lib/capybara/spec/session/body_spec.rb +12 -13
  147. data/lib/capybara/spec/session/check_spec.rb +117 -55
  148. data/lib/capybara/spec/session/choose_spec.rb +64 -31
  149. data/lib/capybara/spec/session/click_button_spec.rb +231 -173
  150. data/lib/capybara/spec/session/click_link_or_button_spec.rb +55 -35
  151. data/lib/capybara/spec/session/click_link_spec.rb +93 -58
  152. data/lib/capybara/spec/session/current_scope_spec.rb +12 -11
  153. data/lib/capybara/spec/session/current_url_spec.rb +57 -39
  154. data/lib/capybara/spec/session/dismiss_confirm_spec.rb +4 -4
  155. data/lib/capybara/spec/session/dismiss_prompt_spec.rb +3 -2
  156. data/lib/capybara/spec/session/element/{assert_match_selector.rb → assert_match_selector_spec.rb} +11 -9
  157. data/lib/capybara/spec/session/element/match_css_spec.rb +18 -10
  158. data/lib/capybara/spec/session/element/match_xpath_spec.rb +9 -7
  159. data/lib/capybara/spec/session/element/matches_selector_spec.rb +71 -57
  160. data/lib/capybara/spec/session/evaluate_async_script_spec.rb +8 -7
  161. data/lib/capybara/spec/session/evaluate_script_spec.rb +29 -8
  162. data/lib/capybara/spec/session/execute_script_spec.rb +10 -8
  163. data/lib/capybara/spec/session/fill_in_spec.rb +134 -43
  164. data/lib/capybara/spec/session/find_button_spec.rb +25 -24
  165. data/lib/capybara/spec/session/find_by_id_spec.rb +10 -9
  166. data/lib/capybara/spec/session/find_field_spec.rb +37 -41
  167. data/lib/capybara/spec/session/find_link_spec.rb +46 -17
  168. data/lib/capybara/spec/session/find_spec.rb +260 -145
  169. data/lib/capybara/spec/session/first_spec.rb +80 -52
  170. data/lib/capybara/spec/session/frame/frame_title_spec.rb +23 -0
  171. data/lib/capybara/spec/session/frame/frame_url_spec.rb +23 -0
  172. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +33 -20
  173. data/lib/capybara/spec/session/frame/within_frame_spec.rb +52 -32
  174. data/lib/capybara/spec/session/go_back_spec.rb +2 -1
  175. data/lib/capybara/spec/session/go_forward_spec.rb +2 -1
  176. data/lib/capybara/spec/session/has_all_selectors_spec.rb +31 -31
  177. data/lib/capybara/spec/session/has_ancestor_spec.rb +46 -0
  178. data/lib/capybara/spec/session/has_any_selectors_spec.rb +29 -0
  179. data/lib/capybara/spec/session/has_button_spec.rb +100 -13
  180. data/lib/capybara/spec/session/has_css_spec.rb +272 -137
  181. data/lib/capybara/spec/session/has_current_path_spec.rb +60 -61
  182. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  183. data/lib/capybara/spec/session/has_field_spec.rb +139 -59
  184. data/lib/capybara/spec/session/has_link_spec.rb +47 -6
  185. data/lib/capybara/spec/session/has_none_selectors_spec.rb +42 -40
  186. data/lib/capybara/spec/session/has_select_spec.rb +107 -72
  187. data/lib/capybara/spec/session/has_selector_spec.rb +120 -71
  188. data/lib/capybara/spec/session/has_sibling_spec.rb +50 -0
  189. data/lib/capybara/spec/session/has_table_spec.rb +183 -5
  190. data/lib/capybara/spec/session/has_text_spec.rb +106 -62
  191. data/lib/capybara/spec/session/has_title_spec.rb +20 -14
  192. data/lib/capybara/spec/session/has_xpath_spec.rb +57 -38
  193. data/lib/capybara/spec/session/{headers.rb → headers_spec.rb} +3 -2
  194. data/lib/capybara/spec/session/html_spec.rb +14 -6
  195. data/lib/capybara/spec/session/matches_style_spec.rb +37 -0
  196. data/lib/capybara/spec/session/node_spec.rb +1024 -153
  197. data/lib/capybara/spec/session/node_wrapper_spec.rb +39 -0
  198. data/lib/capybara/spec/session/refresh_spec.rb +12 -6
  199. data/lib/capybara/spec/session/reset_session_spec.rb +82 -35
  200. data/lib/capybara/spec/session/{response_code.rb → response_code_spec.rb} +2 -1
  201. data/lib/capybara/spec/session/save_and_open_page_spec.rb +3 -2
  202. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +8 -12
  203. data/lib/capybara/spec/session/save_page_spec.rb +42 -55
  204. data/lib/capybara/spec/session/save_screenshot_spec.rb +16 -14
  205. data/lib/capybara/spec/session/screenshot_spec.rb +2 -2
  206. data/lib/capybara/spec/session/scroll_spec.rb +119 -0
  207. data/lib/capybara/spec/session/select_spec.rb +107 -81
  208. data/lib/capybara/spec/session/selectors_spec.rb +52 -19
  209. data/lib/capybara/spec/session/sibling_spec.rb +10 -10
  210. data/lib/capybara/spec/session/text_spec.rb +37 -21
  211. data/lib/capybara/spec/session/title_spec.rb +17 -5
  212. data/lib/capybara/spec/session/uncheck_spec.rb +43 -23
  213. data/lib/capybara/spec/session/unselect_spec.rb +39 -38
  214. data/lib/capybara/spec/session/visit_spec.rb +85 -53
  215. data/lib/capybara/spec/session/window/become_closed_spec.rb +24 -20
  216. data/lib/capybara/spec/session/window/current_window_spec.rb +5 -3
  217. data/lib/capybara/spec/session/window/open_new_window_spec.rb +5 -3
  218. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +27 -22
  219. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +12 -6
  220. data/lib/capybara/spec/session/window/window_spec.rb +97 -63
  221. data/lib/capybara/spec/session/window/windows_spec.rb +12 -10
  222. data/lib/capybara/spec/session/window/within_window_spec.rb +31 -86
  223. data/lib/capybara/spec/session/within_spec.rb +83 -44
  224. data/lib/capybara/spec/spec_helper.rb +54 -44
  225. data/lib/capybara/spec/test_app.rb +158 -43
  226. data/lib/capybara/spec/views/animated.erb +49 -0
  227. data/lib/capybara/spec/views/form.erb +163 -42
  228. data/lib/capybara/spec/views/frame_child.erb +4 -3
  229. data/lib/capybara/spec/views/frame_one.erb +2 -1
  230. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  231. data/lib/capybara/spec/views/frame_two.erb +1 -1
  232. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  233. data/lib/capybara/spec/views/layout.erb +10 -0
  234. data/lib/capybara/spec/views/obscured.erb +47 -0
  235. data/lib/capybara/spec/views/offset.erb +33 -0
  236. data/lib/capybara/spec/views/path.erb +2 -2
  237. data/lib/capybara/spec/views/popup_one.erb +1 -1
  238. data/lib/capybara/spec/views/popup_two.erb +1 -1
  239. data/lib/capybara/spec/views/react.erb +45 -0
  240. data/lib/capybara/spec/views/scroll.erb +21 -0
  241. data/lib/capybara/spec/views/spatial.erb +31 -0
  242. data/lib/capybara/spec/views/tables.erb +68 -1
  243. data/lib/capybara/spec/views/with_animation.erb +81 -0
  244. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  245. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  246. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  247. data/lib/capybara/spec/views/with_hover.erb +3 -2
  248. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  249. data/lib/capybara/spec/views/with_html.erb +69 -12
  250. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  251. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  252. data/lib/capybara/spec/views/with_js.erb +30 -5
  253. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  254. data/lib/capybara/spec/views/with_namespace.erb +21 -0
  255. data/lib/capybara/spec/views/with_scope.erb +2 -2
  256. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  257. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  258. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  259. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  260. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  261. data/lib/capybara/spec/views/with_windows.erb +1 -1
  262. data/lib/capybara/spec/views/within_frames.erb +5 -2
  263. data/lib/capybara/version.rb +2 -1
  264. data/lib/capybara/window.rb +36 -34
  265. data/lib/capybara.rb +134 -107
  266. data/spec/basic_node_spec.rb +60 -34
  267. data/spec/capybara_spec.rb +63 -88
  268. data/spec/counter_spec.rb +35 -0
  269. data/spec/css_builder_spec.rb +101 -0
  270. data/spec/css_splitter_spec.rb +38 -0
  271. data/spec/dsl_spec.rb +85 -64
  272. data/spec/filter_set_spec.rb +27 -9
  273. data/spec/fixtures/certificate.pem +25 -0
  274. data/spec/fixtures/key.pem +27 -0
  275. data/spec/fixtures/selenium_driver_rspec_failure.rb +6 -5
  276. data/spec/fixtures/selenium_driver_rspec_success.rb +6 -5
  277. data/spec/minitest_spec.rb +52 -7
  278. data/spec/minitest_spec_spec.rb +94 -63
  279. data/spec/per_session_config_spec.rb +14 -13
  280. data/spec/rack_test_spec.rb +194 -125
  281. data/spec/regexp_dissassembler_spec.rb +250 -0
  282. data/spec/result_spec.rb +111 -50
  283. data/spec/rspec/features_spec.rb +37 -31
  284. data/spec/rspec/scenarios_spec.rb +10 -8
  285. data/spec/rspec/shared_spec_matchers.rb +473 -422
  286. data/spec/rspec/views_spec.rb +5 -3
  287. data/spec/rspec_matchers_spec.rb +52 -11
  288. data/spec/rspec_spec.rb +109 -89
  289. data/spec/sauce_spec_chrome.rb +43 -0
  290. data/spec/selector_spec.rb +397 -68
  291. data/spec/selenium_spec_chrome.rb +187 -40
  292. data/spec/selenium_spec_chrome_remote.rb +96 -0
  293. data/spec/selenium_spec_edge.rb +60 -0
  294. data/spec/selenium_spec_firefox.rb +201 -41
  295. data/spec/selenium_spec_firefox_remote.rb +94 -0
  296. data/spec/selenium_spec_ie.rb +149 -0
  297. data/spec/selenium_spec_safari.rb +162 -0
  298. data/spec/server_spec.rb +213 -102
  299. data/spec/session_spec.rb +53 -16
  300. data/spec/shared_selenium_node.rb +79 -0
  301. data/spec/shared_selenium_session.rb +473 -122
  302. data/spec/spec_helper.rb +126 -7
  303. data/spec/whitespace_normalizer_spec.rb +54 -0
  304. data/spec/xpath_builder_spec.rb +93 -0
  305. metadata +355 -73
  306. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  307. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  308. data/.yard/yard_extensions.rb +0 -78
  309. data/lib/capybara/query.rb +0 -7
  310. data/lib/capybara/rspec/compound.rb +0 -95
  311. data/lib/capybara/spec/session/assert_current_path.rb +0 -72
  312. data/lib/capybara/spec/session/assert_selector.rb +0 -148
  313. data/lib/capybara/spec/session/assert_text.rb +0 -234
  314. data/lib/capybara/spec/session/source_spec.rb +0 -0
  315. data/lib/capybara/spec/views/with_title.erb +0 -5
  316. data/spec/selenium_spec_marionette.rb +0 -127
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'xpath'
4
+
5
+ module Capybara
6
+ class Selector
7
+ # @api private
8
+ class XPathBuilder
9
+ def initialize(expression)
10
+ @expression = expression || ''
11
+ end
12
+
13
+ attr_reader :expression
14
+
15
+ def add_attribute_conditions(**conditions)
16
+ @expression = conditions.inject(expression) do |xp, (name, value)|
17
+ conditions = name == :class ? class_conditions(value) : attribute_conditions(name => value)
18
+ return xp if conditions.nil?
19
+
20
+ if xp.is_a? XPath::Expression
21
+ xp[conditions]
22
+ else
23
+ "(#{xp})[#{conditions}]"
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def attribute_conditions(attributes)
31
+ attributes.map do |attribute, value|
32
+ case value
33
+ when XPath::Expression
34
+ XPath.attr(attribute)[value]
35
+ when Regexp
36
+ XPath.attr(attribute)[regexp_to_xpath_conditions(value)]
37
+ when true
38
+ XPath.attr(attribute)
39
+ when false, nil
40
+ !XPath.attr(attribute)
41
+ else
42
+ XPath.attr(attribute) == value.to_s
43
+ end
44
+ end.reduce(:&)
45
+ end
46
+
47
+ def class_conditions(classes)
48
+ case classes
49
+ when XPath::Expression, Regexp
50
+ attribute_conditions(class: classes)
51
+ else
52
+ Array(classes).reject { |c| c.is_a? Regexp }.map do |klass|
53
+ if klass.match?(/^!(?!!!)/)
54
+ !XPath.attr(:class).contains_word(klass.slice(1..))
55
+ else
56
+ XPath.attr(:class).contains_word(klass.sub(/^!!/, ''))
57
+ end
58
+ end.reduce(:&)
59
+ end
60
+ end
61
+
62
+ def regexp_to_xpath_conditions(regexp)
63
+ condition = XPath.current
64
+ condition = condition.uppercase if regexp.casefold?
65
+ Selector::RegexpDisassembler.new(regexp).alternated_substrings.map do |strs|
66
+ strs.map { |str| condition.contains(str) }.reduce(:&)
67
+ end.reduce(:|)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,22 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selector/selector'
4
+
1
5
  module Capybara
2
6
  class Selector
3
7
  class CSS
4
8
  def self.escape(str)
5
- out = String.new("")
6
9
  value = str.dup
7
- out << value.slice!(0...1) if value =~ /^[-_]/
8
- out << if value[0] =~ NMSTART
9
- value.slice!(0...1)
10
- else
11
- escape_char(value.slice!(0...1))
12
- end
13
- out << value.gsub(/[^a-zA-Z0-9_-]/) {|c| escape_char c}
10
+ out = +''
11
+ out << value.slice!(0...1) if value.match?(/^[-_]/)
12
+ out << (value[0].match?(NMSTART) ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
13
+ out << value.gsub(/[^a-zA-Z0-9_-]/) { |char| escape_char char }
14
14
  out
15
15
  end
16
16
 
17
- def self.escape_char(c)
18
- return "\\%06x" % c.ord() unless c =~ %r{[ -/:-~]}
19
- "\\#{c}"
17
+ def self.escape_char(char)
18
+ char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06<hex>x', hex: char.ord)
19
+ end
20
+
21
+ def self.split(css)
22
+ Splitter.new.split(css)
20
23
  end
21
24
 
22
25
  S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
@@ -24,7 +27,76 @@ module Capybara
24
27
  UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
25
28
  NONASCII = /[#{S}]/
26
29
  ESCAPE = /#{UNICODE}|\\[ -~#{S}]/
27
- NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
30
+ NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
31
+
32
+ class Splitter
33
+ def split(css)
34
+ selectors = []
35
+ StringIO.open(css.to_s) do |str|
36
+ selector = +''
37
+ while (char = str.getc)
38
+ case char
39
+ when '['
40
+ selector << parse_square(str)
41
+ when '('
42
+ selector << parse_paren(str)
43
+ when '"', "'"
44
+ selector << parse_string(char, str)
45
+ when '\\'
46
+ selector << (char + str.getc)
47
+ when ','
48
+ selectors << selector.strip
49
+ selector.clear
50
+ else
51
+ selector << char
52
+ end
53
+ end
54
+ selectors << selector.strip
55
+ end
56
+ selectors
57
+ end
58
+
59
+ private
60
+
61
+ def parse_square(strio)
62
+ parse_block('[', ']', strio)
63
+ end
64
+
65
+ def parse_paren(strio)
66
+ parse_block('(', ')', strio)
67
+ end
68
+
69
+ def parse_block(start, final, strio)
70
+ block = start
71
+ while (char = strio.getc)
72
+ case char
73
+ when final
74
+ return block + char
75
+ when '\\'
76
+ block += char + strio.getc
77
+ when '"', "'"
78
+ block += parse_string(char, strio)
79
+ else
80
+ block += char
81
+ end
82
+ end
83
+ raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
84
+ end
85
+
86
+ def parse_string(quote, strio)
87
+ string = quote
88
+ while (char = strio.getc)
89
+ string += char
90
+ case char
91
+ when quote
92
+ return string
93
+ when '\\'
94
+ string += strio.getc
95
+ end
96
+ end
97
+ raise ArgumentError, 'Invalid CSS Selector - string end not found'
98
+ end
99
+ end
28
100
  end
29
101
  end
30
- end
102
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
4
+ xpath(:value, :title, :type, :name) do |locator, **options|
5
+ input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
6
+ btn_xpath = XPath.descendant(:button)
7
+ aria_btn_xpath = XPath.descendant[XPath.attr(:role).equals('button')]
8
+ image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
9
+
10
+ unless locator.nil?
11
+ locator = locator.to_s
12
+ locator_matchers = combine_locators(locator, config: self)
13
+ btn_matchers = locator_matchers |
14
+ XPath.string.n.is(locator) |
15
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]
16
+
17
+ label_contains_xpath = locate_label(locator).descendant[labellable_elements]
18
+ input_btn_xpath = input_btn_xpath[locator_matchers]
19
+ btn_xpath = btn_xpath[btn_matchers]
20
+ aria_btn_xpath = aria_btn_xpath[btn_matchers]
21
+
22
+ alt_matches = XPath.attr(:alt).is(locator)
23
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
24
+ image_btn_xpath = image_btn_xpath[alt_matches]
25
+ end
26
+
27
+ btn_xpaths = [input_btn_xpath, btn_xpath, image_btn_xpath, label_contains_xpath].compact
28
+ btn_xpaths << aria_btn_xpath if enable_aria_role
29
+
30
+ %i[value title type].inject(btn_xpaths.inject(&:union)) do |memo, ef|
31
+ memo.where(find_by_attr(ef, options[ef]))
32
+ end
33
+ end
34
+
35
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
36
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
37
+
38
+ node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
39
+ expression_filter(:name) do |xpath, val|
40
+ builder(xpath).add_attribute_conditions(name: val)
41
+ end
42
+
43
+ describe_expression_filters do |disabled: nil, **options|
44
+ desc = +''
45
+ desc << ' that is not disabled' if disabled == false
46
+ desc << describe_all_expression_filters(**options)
47
+ end
48
+
49
+ describe_node_filters do |disabled: nil, **|
50
+ ' that is disabled' if disabled == true
51
+ end
52
+
53
+ def combine_locators(locator, config:)
54
+ [
55
+ XPath.attr(:id).equals(locator),
56
+ XPath.attr(:name).equals(locator),
57
+ XPath.attr(:value).is(locator),
58
+ XPath.attr(:title).is(locator),
59
+ (XPath.attr(:id) == XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for)),
60
+ (XPath.attr(:'aria-label').is(locator) if config.enable_aria_label),
61
+ (XPath.attr(config.test_id) == locator if config.test_id)
62
+ ].compact.inject(&:|)
63
+ end
64
+
65
+ def labellable_elements
66
+ (XPath.self(:input) & XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')) | XPath.self(:button)
67
+ end
68
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
4
+ xpath do |locator, allow_self: nil, **options|
5
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
6
+ XPath.attr(:type) == 'checkbox'
7
+ ]
8
+ locate_field(xpath, locator, **options)
9
+ end
10
+
11
+ filter_set(:_field, %i[checked unchecked disabled name])
12
+
13
+ node_filter(%i[option with]) do |node, value|
14
+ val = node.value
15
+ (value.is_a?(Regexp) ? value.match?(val) : val == value.to_s).tap do |res|
16
+ add_error("Expected value to be #{value.inspect} but it was #{val.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ describe_node_filters do |option: nil, with: nil, **|
21
+ desc = +''
22
+ desc << " with value #{option.inspect}" if option
23
+ desc << " with value #{with.inspect}" if with
24
+ desc
25
+ end
26
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
4
+ css do |css|
5
+ if css.is_a? Symbol
6
+ Capybara::Helpers.warn "DEPRECATED: Passing a symbol (#{css.inspect}) as the CSS locator is deprecated - please pass a string instead : #{Capybara::Helpers.filter_backtrace(caller)}"
7
+ end
8
+ css
9
+ end
10
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
4
+ label 'input box with datalist completion'
5
+
6
+ xpath do |locator, **options|
7
+ xpath = XPath.descendant(:input)[XPath.attr(:list)]
8
+ locate_field(xpath, locator, **options)
9
+ end
10
+
11
+ filter_set(:_field, %i[disabled name placeholder])
12
+
13
+ node_filter(:options) do |node, options|
14
+ actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
15
+ (options.sort == actual.sort).tap do |res|
16
+ add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
17
+ end
18
+ end
19
+
20
+ expression_filter(:with_options) do |expr, options|
21
+ options.inject(expr) do |xpath, option|
22
+ xpath.where(XPath.attr(:list) == XPath.anywhere(:datalist)[expression_for(:datalist_option, option)].attr(:id))
23
+ end
24
+ end
25
+
26
+ describe_expression_filters do |with_options: nil, **|
27
+ desc = +''
28
+ desc << " with at least options #{with_options.inspect}" if with_options
29
+ desc
30
+ end
31
+
32
+ describe_node_filters do |options: nil, **|
33
+ " with options #{options.inspect}" if options
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
4
+ label 'datalist option'
5
+ visible(:all)
6
+
7
+ xpath do |locator|
8
+ xpath = XPath.descendant(:option)
9
+ xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil?
10
+ xpath
11
+ end
12
+
13
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
14
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
15
+
16
+ describe_expression_filters do |disabled: nil, **options|
17
+ desc = +''
18
+ desc << ' that is not disabled' if disabled == false
19
+ desc << describe_all_expression_filters(**options)
20
+ end
21
+
22
+ describe_node_filters do |**options|
23
+ ' that is disabled' if options[:disabled]
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
4
+ xpath do |locator, **|
5
+ XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
6
+ end
7
+
8
+ expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
9
+ builder(xpath).add_attribute_conditions(name => val)
10
+ end
11
+
12
+ node_filter(:attributes, matcher: /.+/) do |node, name, val|
13
+ next true unless val.is_a?(Regexp)
14
+
15
+ (val.match? node[name]).tap do |res|
16
+ add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
17
+ end
18
+ end
19
+
20
+ describe_expression_filters do |**options|
21
+ boolean_values = [true, false]
22
+ booleans, values = options.partition { |_k, v| boolean_values.include? v }.map(&:to_h)
23
+ desc = describe_all_expression_filters(**values)
24
+ desc + booleans.map do |k, v|
25
+ v ? " with #{k} attribute" : "without #{k} attribute"
26
+ end.join
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
4
+ visible { |options| :hidden if options[:type].to_s == 'hidden' }
5
+
6
+ xpath do |locator, **options|
7
+ invalid_types = %w[submit image]
8
+ invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
9
+ xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
10
+ locate_field(xpath, locator, **options)
11
+ end
12
+
13
+ expression_filter(:type) do |expr, type|
14
+ type = type.to_s
15
+ if %w[textarea select].include?(type)
16
+ expr.self(type.to_sym)
17
+ else
18
+ expr[XPath.attr(:type) == type]
19
+ end
20
+ end
21
+
22
+ filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
23
+
24
+ node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
25
+
26
+ node_filter(:with) do |node, with|
27
+ val = node.value
28
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
29
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
30
+ end
31
+ end
32
+
33
+ describe_expression_filters do |type: nil, **|
34
+ " of type #{type.inspect}" if type
35
+ end
36
+
37
+ describe_node_filters do |**options|
38
+ " with value #{options[:with].to_s.inspect}" if options.key?(:with)
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
4
+ xpath do |locator, legend: nil, **|
5
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
6
+ locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
7
+ xpath = XPath.descendant(:fieldset)[locator && locator_matchers]
8
+ xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend
9
+ xpath
10
+ end
11
+
12
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
13
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
4
+ label 'file field'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input)[
7
+ XPath.attr(:type) == 'file'
8
+ ]
9
+ locate_field(xpath, locator, **options)
10
+ end
11
+
12
+ filter_set(:_field, %i[disabled multiple name])
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
4
+ label 'field'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :'descendant-or-self' : :descendant, :input, :textarea)[
7
+ !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
8
+ ]
9
+ locate_field(xpath, locator, **options)
10
+ end
11
+
12
+ expression_filter(:type) do |expr, type|
13
+ type = type.to_s
14
+ if type == 'textarea'
15
+ expr.self(type.to_sym)
16
+ else
17
+ expr[XPath.attr(:type) == type]
18
+ end
19
+ end
20
+
21
+ filter_set(:_field, %i[disabled multiple name placeholder valid validation_message])
22
+
23
+ node_filter(:with) do |node, with|
24
+ val = node.value
25
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
26
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
27
+ end
28
+ end
29
+
30
+ describe_node_filters do |**options|
31
+ " with value #{options[:with].to_s.inspect}" if options.key?(:with)
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
4
+ xpath do |locator, name: nil, **|
5
+ xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
6
+ unless locator.nil?
7
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)
8
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
9
+ xpath = xpath[locator_matchers]
10
+ end
11
+ xpath[find_by_attr(:name, name)]
12
+ end
13
+
14
+ describe_expression_filters do |name: nil, **|
15
+ " with name #{name}" if name
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
4
+ xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
5
+ locator_filter { |node, id| id.is_a?(Regexp) ? id.match?(node[:id]) : true }
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
4
+ label 'label'
5
+ xpath(:for) do |locator, **options|
6
+ xpath = XPath.descendant(:label)
7
+ unless locator.nil?
8
+ locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
9
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
10
+ xpath = xpath[locator_matchers]
11
+ end
12
+ if options.key?(:for)
13
+ for_option = options[:for]
14
+ for_option = for_option[:id] if for_option.is_a?(Capybara::Node::Element)
15
+ if for_option && (for_option != '')
16
+ with_attr = builder(XPath.self).add_attribute_conditions(for: for_option)
17
+ wrapped = !XPath.attr(:for) &
18
+ builder(XPath.self.descendant(*labelable_elements)).add_attribute_conditions(id: for_option)
19
+ xpath = xpath[with_attr | wrapped]
20
+ end
21
+ end
22
+ xpath
23
+ end
24
+
25
+ node_filter(:for) do |node, field_or_value|
26
+ case field_or_value
27
+ when Capybara::Node::Element
28
+ if (for_val = node[:for])
29
+ field_or_value[:id] == for_val
30
+ else
31
+ field_or_value.find_xpath('./ancestor::label[1]').include? node.base
32
+ end
33
+ when Regexp
34
+ if (for_val = node[:for])
35
+ field_or_value.match? for_val
36
+ else
37
+ node.find_xpath(XPath.descendant(*labelable_elements).to_s)
38
+ .any? { |n| field_or_value.match? n[:id] }
39
+ end
40
+ else
41
+ # Non element/regexp values were handled through the expression filter
42
+ true
43
+ end
44
+ end
45
+
46
+ describe_expression_filters do |**options|
47
+ next unless options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
48
+
49
+ if options[:for].is_a? Regexp
50
+ " for element with id matching #{options[:for].inspect}"
51
+ else
52
+ " for element with id of \"#{options[:for]}\""
53
+ end
54
+ end
55
+ describe_node_filters do |**options|
56
+ " for element #{options[:for]}" if options[:for].is_a?(Capybara::Node::Element)
57
+ end
58
+
59
+ def labelable_elements
60
+ %i[button input keygen meter output progress select textarea]
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
4
+ xpath do |locator, href: true, alt: nil, title: nil, target: nil, **|
5
+ xpath = XPath.descendant(:a)
6
+ xpath = builder(xpath).add_attribute_conditions(href: href) unless href == false
7
+
8
+ if enable_aria_role
9
+ role_path = XPath.descendant[XPath.attr(:role).equals('link')]
10
+ role_path = builder(role_path).add_attribute_conditions(href: href) unless [true, false].include? href
11
+
12
+ xpath += role_path
13
+ end
14
+
15
+ unless locator.nil?
16
+ locator = locator.to_s
17
+ matchers = [XPath.attr(:id) == locator,
18
+ XPath.string.n.is(locator),
19
+ XPath.attr(:title).is(locator),
20
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
21
+ matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
22
+ matchers << XPath.attr(test_id).equals(locator) if test_id
23
+ xpath = xpath[matchers.reduce(:|)]
24
+ end
25
+
26
+ xpath = xpath[find_by_attr(:title, title)]
27
+ xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
28
+ xpath = xpath[find_by_attr(:target, target)] if target
29
+
30
+ xpath
31
+ end
32
+
33
+ node_filter(:href) do |node, href|
34
+ # If not a Regexp it's been handled in the main XPath
35
+ (href.is_a?(Regexp) ? node[:href].match?(href) : true).tap do |res|
36
+ add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
37
+ end
38
+ end
39
+
40
+ expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
41
+ builder(expr).add_attribute_conditions(download: download)
42
+ end
43
+
44
+ describe_expression_filters do |download: nil, **options|
45
+ desc = +''
46
+ if (href = options[:href])
47
+ desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
48
+ elsif options.key?(:href) && href != false # is nil specified?
49
+ desc << ' with no href attribute'
50
+ end
51
+ desc << " with download attribute#{" #{download}" if download.is_a? String}" if download
52
+ desc << ' without download attribute' if download == false
53
+ desc
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
4
+ label 'link or button'
5
+ xpath do |locator, **options|
6
+ %i[link button].map do |selector|
7
+ expression_for(selector, locator, **options)
8
+ end.reduce(:union)
9
+ end
10
+
11
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
12
+
13
+ describe_node_filters do |disabled: nil, **|
14
+ ' that is disabled' if disabled == true
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:option, locator_type: [String, Symbol, Integer]) do
4
+ xpath do |locator|
5
+ xpath = XPath.descendant(:option)
6
+ xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
7
+ xpath
8
+ end
9
+
10
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
11
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
12
+
13
+ node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
14
+
15
+ describe_expression_filters do |disabled: nil, **options|
16
+ desc = +''
17
+ desc << ' that is not disabled' if disabled == false
18
+ (expression_filters.keys & options.keys).inject(desc) { |memo, ef| memo << " with #{ef} #{options[ef]}" }
19
+ end
20
+
21
+ describe_node_filters do |**options|
22
+ desc = +''
23
+ desc << ' that is disabled' if options[:disabled]
24
+ desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
25
+ desc
26
+ end
27
+ end