capybara 3.3.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 (308) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/History.md +803 -13
  4. data/License.txt +1 -1
  5. data/README.md +257 -84
  6. data/lib/capybara/config.rb +25 -9
  7. data/lib/capybara/cucumber.rb +1 -1
  8. data/lib/capybara/driver/base.rb +17 -3
  9. data/lib/capybara/driver/node.rb +31 -6
  10. data/lib/capybara/dsl.rb +9 -7
  11. data/lib/capybara/helpers.rb +31 -7
  12. data/lib/capybara/minitest/spec.rb +180 -88
  13. data/lib/capybara/minitest.rb +262 -149
  14. data/lib/capybara/node/actions.rb +202 -116
  15. data/lib/capybara/node/base.rb +34 -19
  16. data/lib/capybara/node/document.rb +14 -2
  17. data/lib/capybara/node/document_matchers.rb +10 -12
  18. data/lib/capybara/node/element.rb +269 -115
  19. data/lib/capybara/node/finders.rb +99 -77
  20. data/lib/capybara/node/matchers.rb +327 -151
  21. data/lib/capybara/node/simple.rb +48 -13
  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 +8 -9
  25. data/lib/capybara/queries/base_query.rb +23 -16
  26. data/lib/capybara/queries/current_path_query.rb +16 -6
  27. data/lib/capybara/queries/match_query.rb +1 -0
  28. data/lib/capybara/queries/selector_query.rb +587 -130
  29. data/lib/capybara/queries/sibling_query.rb +8 -6
  30. data/lib/capybara/queries/style_query.rb +6 -2
  31. data/lib/capybara/queries/text_query.rb +28 -14
  32. data/lib/capybara/queries/title_query.rb +2 -2
  33. data/lib/capybara/rack_test/browser.rb +92 -25
  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 +68 -41
  37. data/lib/capybara/rack_test/node.rb +106 -39
  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 +75 -52
  44. data/lib/capybara/rspec/features.rb +7 -7
  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 +141 -339
  60. data/lib/capybara/rspec.rb +2 -0
  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 +27 -25
  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 +73 -25
  88. data/lib/capybara/selector/filters/base.rb +24 -5
  89. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  90. data/lib/capybara/selector/filters/locator_filter.rb +29 -0
  91. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  92. data/lib/capybara/selector/regexp_disassembler.rb +211 -0
  93. data/lib/capybara/selector/selector.rb +85 -348
  94. data/lib/capybara/selector/xpath_extensions.rb +17 -0
  95. data/lib/capybara/selector.rb +474 -447
  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 +255 -143
  101. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +93 -11
  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 +436 -134
  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 +56 -19
  123. data/lib/capybara/server/checker.rb +9 -3
  124. data/lib/capybara/server/middleware.rb +28 -12
  125. data/lib/capybara/server.rb +33 -10
  126. data/lib/capybara/session/config.rb +34 -10
  127. data/lib/capybara/session/matchers.rb +23 -16
  128. data/lib/capybara/session.rb +230 -170
  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 +121 -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 +127 -40
  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_spec.rb +21 -18
  140. data/lib/capybara/spec/session/assert_selector_spec.rb +52 -58
  141. data/lib/capybara/spec/session/assert_style_spec.rb +7 -7
  142. data/lib/capybara/spec/session/assert_text_spec.rb +74 -50
  143. data/lib/capybara/spec/session/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 +6 -6
  146. data/lib/capybara/spec/session/check_spec.rb +102 -47
  147. data/lib/capybara/spec/session/choose_spec.rb +58 -32
  148. data/lib/capybara/spec/session/click_button_spec.rb +219 -163
  149. data/lib/capybara/spec/session/click_link_or_button_spec.rb +49 -23
  150. data/lib/capybara/spec/session/click_link_spec.rb +77 -54
  151. data/lib/capybara/spec/session/current_scope_spec.rb +8 -8
  152. data/lib/capybara/spec/session/current_url_spec.rb +38 -29
  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_spec.rb +8 -8
  156. data/lib/capybara/spec/session/element/match_css_spec.rb +16 -10
  157. data/lib/capybara/spec/session/element/match_xpath_spec.rb +6 -6
  158. data/lib/capybara/spec/session/element/matches_selector_spec.rb +68 -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 +101 -46
  163. data/lib/capybara/spec/session/find_button_spec.rb +23 -23
  164. data/lib/capybara/spec/session/find_by_id_spec.rb +7 -7
  165. data/lib/capybara/spec/session/find_field_spec.rb +32 -30
  166. data/lib/capybara/spec/session/find_link_spec.rb +31 -21
  167. data/lib/capybara/spec/session/find_spec.rb +244 -141
  168. data/lib/capybara/spec/session/first_spec.rb +43 -43
  169. data/lib/capybara/spec/session/frame/frame_title_spec.rb +5 -5
  170. data/lib/capybara/spec/session/frame/frame_url_spec.rb +5 -5
  171. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +30 -18
  172. data/lib/capybara/spec/session/frame/within_frame_spec.rb +45 -18
  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 +94 -13
  179. data/lib/capybara/spec/session/has_css_spec.rb +272 -132
  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 +44 -4
  184. data/lib/capybara/spec/session/has_none_selectors_spec.rb +31 -31
  185. data/lib/capybara/spec/session/has_select_spec.rb +84 -50
  186. data/lib/capybara/spec/session/has_selector_spec.rb +111 -71
  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_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 +894 -142
  196. data/lib/capybara/spec/session/node_wrapper_spec.rb +10 -7
  197. data/lib/capybara/spec/session/refresh_spec.rb +9 -7
  198. data/lib/capybara/spec/session/reset_session_spec.rb +63 -35
  199. data/lib/capybara/spec/session/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 +2 -2
  202. data/lib/capybara/spec/session/save_page_spec.rb +37 -37
  203. data/lib/capybara/spec/session/save_screenshot_spec.rb +10 -10
  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 +85 -85
  207. data/lib/capybara/spec/session/selectors_spec.rb +49 -18
  208. data/lib/capybara/spec/session/sibling_spec.rb +9 -9
  209. data/lib/capybara/spec/session/text_spec.rb +25 -24
  210. data/lib/capybara/spec/session/title_spec.rb +7 -6
  211. data/lib/capybara/spec/session/uncheck_spec.rb +33 -21
  212. data/lib/capybara/spec/session/unselect_spec.rb +37 -37
  213. data/lib/capybara/spec/session/visit_spec.rb +68 -49
  214. data/lib/capybara/spec/session/window/become_closed_spec.rb +20 -17
  215. data/lib/capybara/spec/session/window/current_window_spec.rb +1 -1
  216. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +20 -16
  217. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +6 -2
  218. data/lib/capybara/spec/session/window/window_spec.rb +62 -63
  219. data/lib/capybara/spec/session/window/windows_spec.rb +5 -1
  220. data/lib/capybara/spec/session/window/within_window_spec.rb +14 -14
  221. data/lib/capybara/spec/session/within_spec.rb +79 -42
  222. data/lib/capybara/spec/spec_helper.rb +41 -53
  223. data/lib/capybara/spec/test_app.rb +132 -43
  224. data/lib/capybara/spec/views/animated.erb +49 -0
  225. data/lib/capybara/spec/views/form.erb +139 -42
  226. data/lib/capybara/spec/views/frame_child.erb +4 -3
  227. data/lib/capybara/spec/views/frame_one.erb +2 -1
  228. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  229. data/lib/capybara/spec/views/frame_two.erb +1 -1
  230. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  231. data/lib/capybara/spec/views/layout.erb +10 -0
  232. data/lib/capybara/spec/views/obscured.erb +47 -0
  233. data/lib/capybara/spec/views/offset.erb +33 -0
  234. data/lib/capybara/spec/views/path.erb +2 -2
  235. data/lib/capybara/spec/views/popup_one.erb +1 -1
  236. data/lib/capybara/spec/views/popup_two.erb +1 -1
  237. data/lib/capybara/spec/views/react.erb +45 -0
  238. data/lib/capybara/spec/views/scroll.erb +21 -0
  239. data/lib/capybara/spec/views/spatial.erb +31 -0
  240. data/lib/capybara/spec/views/tables.erb +67 -0
  241. data/lib/capybara/spec/views/with_animation.erb +39 -4
  242. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  243. data/lib/capybara/spec/views/with_dragula.erb +24 -0
  244. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  245. data/lib/capybara/spec/views/with_hover.erb +3 -2
  246. data/lib/capybara/spec/views/with_hover1.erb +10 -0
  247. data/lib/capybara/spec/views/with_html.erb +37 -9
  248. data/lib/capybara/spec/views/with_html5_svg.erb +20 -0
  249. data/lib/capybara/spec/views/with_jquery_animation.erb +24 -0
  250. data/lib/capybara/spec/views/with_js.erb +26 -5
  251. data/lib/capybara/spec/views/with_jstree.erb +26 -0
  252. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  253. data/lib/capybara/spec/views/with_scope.erb +2 -2
  254. data/lib/capybara/spec/views/with_scope_other.erb +6 -0
  255. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  256. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  257. data/lib/capybara/spec/views/with_sortable_js.erb +21 -0
  258. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  259. data/lib/capybara/spec/views/with_windows.erb +1 -1
  260. data/lib/capybara/spec/views/within_frames.erb +1 -1
  261. data/lib/capybara/version.rb +1 -1
  262. data/lib/capybara/window.rb +19 -25
  263. data/lib/capybara.rb +126 -111
  264. data/spec/basic_node_spec.rb +59 -34
  265. data/spec/capybara_spec.rb +56 -44
  266. data/spec/counter_spec.rb +35 -0
  267. data/spec/css_builder_spec.rb +101 -0
  268. data/spec/css_splitter_spec.rb +8 -8
  269. data/spec/dsl_spec.rb +79 -52
  270. data/spec/filter_set_spec.rb +9 -9
  271. data/spec/fixtures/selenium_driver_rspec_failure.rb +4 -4
  272. data/spec/fixtures/selenium_driver_rspec_success.rb +4 -4
  273. data/spec/minitest_spec.rb +45 -7
  274. data/spec/minitest_spec_spec.rb +87 -64
  275. data/spec/per_session_config_spec.rb +6 -6
  276. data/spec/rack_test_spec.rb +172 -116
  277. data/spec/regexp_dissassembler_spec.rb +250 -0
  278. data/spec/result_spec.rb +80 -72
  279. data/spec/rspec/features_spec.rb +21 -16
  280. data/spec/rspec/scenarios_spec.rb +10 -6
  281. data/spec/rspec/shared_spec_matchers.rb +407 -365
  282. data/spec/rspec/views_spec.rb +3 -3
  283. data/spec/rspec_matchers_spec.rb +35 -10
  284. data/spec/rspec_spec.rb +63 -41
  285. data/spec/sauce_spec_chrome.rb +43 -0
  286. data/spec/selector_spec.rb +334 -89
  287. data/spec/selenium_spec_chrome.rb +176 -62
  288. data/spec/selenium_spec_chrome_remote.rb +54 -14
  289. data/spec/selenium_spec_edge.rb +41 -8
  290. data/spec/selenium_spec_firefox.rb +228 -0
  291. data/spec/selenium_spec_firefox_remote.rb +94 -0
  292. data/spec/selenium_spec_ie.rb +129 -11
  293. data/spec/selenium_spec_safari.rb +162 -0
  294. data/spec/server_spec.rb +171 -97
  295. data/spec/session_spec.rb +34 -18
  296. data/spec/shared_selenium_node.rb +79 -0
  297. data/spec/shared_selenium_session.rb +344 -80
  298. data/spec/spec_helper.rb +124 -2
  299. data/spec/whitespace_normalizer_spec.rb +54 -0
  300. data/spec/xpath_builder_spec.rb +93 -0
  301. metadata +326 -28
  302. data/lib/capybara/rspec/compound.rb +0 -94
  303. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +0 -31
  304. data/lib/capybara/selenium/nodes/marionette_node.rb +0 -31
  305. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
  306. data/lib/capybara/spec/session/source_spec.rb +0 -0
  307. data/lib/capybara/spec/views/with_title.erb +0 -5
  308. data/spec/selenium_spec_marionette.rb +0 -167
@@ -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,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selector/selector'
4
+
3
5
  module Capybara
4
6
  class Selector
5
7
  class CSS
6
8
  def self.escape(str)
7
9
  value = str.dup
8
- out = +""
9
- out << value.slice!(0...1) if value =~ /^[-_]/
10
- out << (value[0] =~ NMSTART ? value.slice!(0...1) : escape_char(value.slice!(0...1)))
11
- 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 }
12
14
  out
13
15
  end
14
16
 
15
- def self.escape_char(c)
16
- c =~ %r{[ -/:-~]} ? "\\#{c}" : format("\\%06x", c.ord)
17
+ def self.escape_char(char)
18
+ char.match?(%r{[ -/:-~]}) ? "\\#{char}" : format('\\%06<hex>x', hex: char.ord)
17
19
  end
18
20
 
19
21
  def self.split(css)
@@ -30,23 +32,23 @@ module Capybara
30
32
  class Splitter
31
33
  def split(css)
32
34
  selectors = []
33
- StringIO.open(css) do |str|
34
- selector = ""
35
- while (c = str.getc)
36
- case c
35
+ StringIO.open(css.to_s) do |str|
36
+ selector = +''
37
+ while (char = str.getc)
38
+ case char
37
39
  when '['
38
- selector += parse_square(str)
40
+ selector << parse_square(str)
39
41
  when '('
40
- selector += parse_paren(str)
42
+ selector << parse_paren(str)
41
43
  when '"', "'"
42
- selector += parse_string(c, str)
44
+ selector << parse_string(char, str)
43
45
  when '\\'
44
- selector += c + str.getc
46
+ selector << (char + str.getc)
45
47
  when ','
46
48
  selectors << selector.strip
47
- selector = ""
49
+ selector.clear
48
50
  else
49
- selector += c
51
+ selector << char
50
52
  end
51
53
  end
52
54
  selectors << selector.strip
@@ -66,16 +68,16 @@ module Capybara
66
68
 
67
69
  def parse_block(start, final, strio)
68
70
  block = start
69
- while (c = strio.getc)
70
- case c
71
+ while (char = strio.getc)
72
+ case char
71
73
  when final
72
- return block + c
74
+ return block + char
73
75
  when '\\'
74
- block += c + strio.getc
76
+ block += char + strio.getc
75
77
  when '"', "'"
76
- block += parse_string(c, strio)
78
+ block += parse_string(char, strio)
77
79
  else
78
- block += c
80
+ block += char
79
81
  end
80
82
  end
81
83
  raise ArgumentError, "Invalid CSS Selector - Block end '#{final}' not found"
@@ -83,9 +85,9 @@ module Capybara
83
85
 
84
86
  def parse_string(quote, strio)
85
87
  string = quote
86
- while (c = strio.getc)
87
- string += c
88
- case c
88
+ while (char = strio.getc)
89
+ string += char
90
+ case char
89
91
  when quote
90
92
  return string
91
93
  when '\\'
@@ -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