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
@@ -1,542 +1,494 @@
1
1
  # frozen_string_literal: true
2
- require 'capybara/selector/selector'
3
- Capybara::Selector::FilterSet.add(:_field) do
4
- filter(:checked, :boolean) { |node, value| not(value ^ node.checked?) }
5
- filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
6
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
7
- filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
8
-
9
- expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name).equals(val)] }
10
- expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder).equals(val)] }
11
-
12
- describe do |options|
13
- desc, states = String.new, []
14
- states << 'checked' if options[:checked] || (options[:unchecked] == false)
15
- states << 'not checked' if options[:unchecked] || (options[:checked] == false)
16
- states << 'disabled' if options[:disabled] == true
17
- states << 'not disabled' if options[:disabled] == false
18
- desc << " that is #{states.join(' and ')}" unless states.empty?
19
- desc << " with the multiple attribute" if options[:multiple] == true
20
- desc << " without the multiple attribute" if options[:multiple] == false
21
- desc
22
- end
23
- end
24
-
25
- ##
26
- #
27
- # Select elements by XPath expression
28
- #
29
- # @locator An XPath expression
30
- #
31
- Capybara.add_selector(:xpath) do
32
- xpath { |xpath| xpath }
33
- end
34
-
35
- ##
36
- #
37
- # Select elements by CSS selector
38
- #
39
- # @locator A CSS selector
40
- #
41
- Capybara.add_selector(:css) do
42
- css { |css| css }
43
- end
44
-
45
- ##
46
- #
47
- # Select element by id
48
- #
49
- # @locator The id of the element to match
50
- #
51
- Capybara.add_selector(:id) do
52
- xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
53
- end
54
-
55
- ##
56
- #
57
- # Select field elements (input [not of type submit, image, or hidden], textarea, select)
58
- #
59
- # @locator Matches against the id, name, or placeholder
60
- # @filter [String] :id Matches the id attribute
61
- # @filter [String] :name Matches the name attribute
62
- # @filter [String] :placeholder Matches the placeholder attribute
63
- # @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select'
64
- # @filter [Boolean] :readonly
65
- # @filter [String] :with Matches the current value of the field
66
- # @filter [String, Array<String>] :class Matches the class(es) provided
67
- # @filter [Boolean] :checked Match checked fields?
68
- # @filter [Boolean] :unchecked Match unchecked fields?
69
- # @filter [Boolean] :disabled Match disabled field?
70
- # @filter [Boolean] :multiple Match fields that accept multiple values
71
- Capybara.add_selector(:field) do
72
- xpath do |locator, options|
73
- xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
74
- locate_field(xpath, locator, options)
75
- end
76
-
77
- expression_filter(:type) do |expr, type|
78
- type = type.to_s
79
- if ['textarea', 'select'].include?(type)
80
- expr.axis(:self, type.to_sym)
81
- else
82
- expr[XPath.attr(:type).equals(type)]
83
- end
84
- end
85
-
86
- filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
87
-
88
- filter(:readonly, :boolean) { |node, value| not(value ^ node.readonly?) }
89
- filter(:with) do |node, with|
90
- with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
91
- end
92
- describe do |options|
93
- desc = String.new
94
- (expression_filters.keys - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.has_key?(ef) }
95
- desc << " of type #{options[:type].inspect}" if options[:type]
96
- desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
97
- desc
98
- end
99
- end
100
-
101
- ##
102
- #
103
- # Select fieldset elements
104
- #
105
- # @locator Matches id or contents of wrapped legend
106
- #
107
- # @filter [String] :id Matches id attribute
108
- # @filter [String] :legend Matches contents of wrapped legend
109
- # @filter [String, Array<String>] :class Matches the class(es) provided
110
- #
111
- Capybara.add_selector(:fieldset) do
112
- xpath(:legend) do |locator, options|
113
- xpath = XPath.descendant(:fieldset)
114
- xpath = xpath[XPath.attr(:id).equals(locator.to_s).or XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil?
115
- xpath = xpath[XPath.child(:legend)[XPath.string.n.is(options[:legend])]] if options[:legend]
116
- xpath
117
- end
118
- end
119
-
120
- ##
121
- #
122
- # Find links ( <a> elements with an href attribute )
123
- #
124
- # @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
125
- #
126
- # @filter [String] :id Matches the id attribute
127
- # @filter [String] :title Matches the title attribute
128
- # @filter [String] :alt Matches the alt attribute of a contained img element
129
- # @filter [String] :class Matches the class(es) provided
130
- # @filter [String, Regexp,nil] :href Matches the normalized href of the link, if nil will find <a> elements with no href attribute
131
- #
132
- Capybara.add_selector(:link) do
133
- xpath(:title, :alt) do |locator, options={}|
134
- xpath = XPath.descendant(:a)
135
- xpath = if options.fetch(:href, true).nil?
136
- xpath[~XPath.attr(:href)]
137
- else
138
- xpath[XPath.attr(:href)]
139
- end
140
- unless locator.nil?
141
- locator = locator.to_s
142
- matchers = XPath.attr(:id).equals(locator).or(
143
- XPath.string.n.is(locator)).or(
144
- XPath.attr(:title).is(locator)).or(
145
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)])
146
- matchers = matchers.or XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
147
- xpath = xpath[matchers]
148
- end
149
- xpath = [:title].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
150
- xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt).equals(options[:alt])]] if options[:alt]
151
- xpath
152
- end
153
-
154
- filter(:href) do |node, href|
155
- case href
156
- when nil
157
- true
158
- when Regexp
159
- node[:href].match href
160
- else
161
- node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href.to_s)], minimum: 0)
162
- end
163
- end
164
-
165
- describe do |options|
166
- desc = String.new()
167
- desc << " with href #{options[:href].inspect}" if options[:href]
168
- desc << " with no href attribute" if options.fetch(:href, true).nil?
169
- end
170
- end
171
2
 
172
- ##
173
- #
174
- # Find buttons ( input [of type submit, reset, image, button] or button elements )
175
- #
176
- # @locator Matches the id, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
177
- #
178
- # @filter [String] :id Matches the id attribute
179
- # @filter [String] :title Matches the title attribute
180
- # @filter [String] :class Matches the class(es) provided
181
- # @filter [String] :value Matches the value of an input button
3
+ require 'capybara/selector/xpath_extensions'
4
+ require 'capybara/selector/selector'
5
+ require 'capybara/selector/definition'
6
+
7
+ #
8
+ # All Selectors below support the listed selector specific filters in addition to the following system-wide filters
9
+ # * :id (String, Regexp, XPath::Expression) - Matches the id attribute
10
+ # * :class (String, Array<String | Regexp>, Regexp, XPath::Expression) - Matches the class(es) provided
11
+ # * :style (String, Regexp, Hash<String, String>) - Match on elements style
12
+ # * :above (Element) - Match elements above the passed element on the page
13
+ # * :below (Element) - Match elements below the passed element on the page
14
+ # * :left_of (Element) - Match elements left of the passed element on the page
15
+ # * :right_of (Element) - Match elements right of the passed element on the page
16
+ # * :near (Element) - Match elements near (within 50px) the passed element on the page
17
+ # * :focused (Boolean) - Match elements with focus (requires driver support)
18
+ #
19
+ # ### Built-in Selectors
20
+ #
21
+ # * **:xpath** - Select elements by XPath expression
22
+ # * Locator: An XPath expression
23
+ #
24
+ # ```ruby
25
+ # page.html # => '<input>'
26
+ #
27
+ # page.find :xpath, './/input'
28
+ # ```
29
+ #
30
+ # * **:css** - Select elements by CSS selector
31
+ # * Locator: A CSS selector
32
+ #
33
+ # ```ruby
34
+ # page.html # => '<input>'
182
35
  #
183
- Capybara.add_selector(:button) do
184
- xpath(:value, :title) do |locator, options={}|
185
- input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
186
- btn_xpath = XPath.descendant(:button)
187
- image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')]
188
-
189
- unless locator.nil?
190
- locator = locator.to_s
191
- locator_matches = XPath.attr(:id).equals(locator).or XPath.attr(:value).is(locator).or XPath.attr(:title).is(locator)
192
- locator_matches = locator_matches.or XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
193
-
194
- input_btn_xpath = input_btn_xpath[locator_matches]
195
-
196
- btn_xpath = btn_xpath[locator_matches.or XPath.string.n.is(locator).or XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
197
-
198
- alt_matches = XPath.attr(:alt).is(locator)
199
- alt_matches = alt_matches.or XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
200
- image_btn_xpath = image_btn_xpath[alt_matches]
201
- end
202
-
203
- res_xpath = input_btn_xpath.union(btn_xpath).union(image_btn_xpath)
204
-
205
- res_xpath = expression_filters.keys.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
206
-
207
- res_xpath
208
- end
209
-
210
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
211
-
212
- describe do |options|
213
- desc = String.new
214
- desc << " that is disabled" if options[:disabled] == true
215
- desc << describe_all_expression_filters(options)
216
- desc
217
- end
218
- end
219
-
220
- ##
36
+ # page.find :css, 'input'
37
+ # ```
221
38
  #
222
- # Find links or buttons
39
+ # * **:id** - Select element by id
40
+ # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
223
41
  #
224
- Capybara.add_selector(:link_or_button) do
225
- label "link or button"
226
- xpath do |locator, options|
227
- self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator, options)}.reduce(:union)
228
- end
229
-
230
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
231
-
232
- describe { |options| " that is disabled" if options[:disabled] }
233
- end
234
-
235
- ##
236
- #
237
- # Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
238
- #
239
- # @locator Matches against the id, name, or placeholder
240
- # @filter [String] :id Matches the id attribute
241
- # @filter [String] :name Matches the name attribute
242
- # @filter [String] :placeholder Matches the placeholder attribute
243
- # @filter [String] :with Matches the current value of the field
244
- # @filter [String] :type Matches the type attribute of the field or element type for 'textarea'
245
- # @filter [String, Array<String>] :class Matches the class(es) provided
246
- # @filter [Boolean] :disabled Match disabled field?
247
- # @filter [Boolean] :multiple Match fields that accept multiple values
248
- #
249
- Capybara.add_selector(:fillable_field) do
250
- label "field"
251
- xpath do |locator, options|
252
- xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
253
- locate_field(xpath, locator, options)
254
- end
42
+ # ```ruby
43
+ # page.html # => '<input id="field">'
44
+ #
45
+ # page.find :id, 'content'
46
+ # ```
47
+ #
48
+ # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
49
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or
50
+ # associated label text
51
+ # * Filters:
52
+ # * :name (String, Regexp) - Matches the name attribute
53
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
54
+ # * :type (String) - Matches the type attribute of the field or element type for 'textarea' and 'select'
55
+ # * :readonly (Boolean) - Match on the element being readonly
56
+ # * :with (String, Regexp) - Matches the current value of the field
57
+ # * :checked (Boolean) - Match checked fields?
58
+ # * :unchecked (Boolean) - Match unchecked fields?
59
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
60
+ # * :multiple (Boolean) - Match fields that accept multiple values
61
+ # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
62
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
63
+ #
64
+ # ```ruby
65
+ # page.html # => '<label for="article_title">Title</label>
66
+ # <input id="article_title" name="article[title]" value="Hello world">'
67
+ #
68
+ # page.find :field, 'article_title'
69
+ # page.find :field, 'article[title]'
70
+ # page.find :field, 'Title'
71
+ # page.find :field, 'Title', type: 'text', with: 'Hello world'
72
+ # ```
73
+ #
74
+ # * **:fieldset** - Select fieldset elements
75
+ # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
76
+ # * Filters:
77
+ # * :legend (String) - Matches contents of wrapped legend
78
+ # * :disabled (Boolean) - Match disabled fieldset?
79
+ #
80
+ # ```ruby
81
+ # page.html # => '<fieldset disabled>
82
+ # <legend>Fields (disabled)</legend>
83
+ # </fieldset>'
84
+ #
85
+ # page.find :fieldset, 'Fields (disabled)', disabled: true
86
+ # ```
87
+ #
88
+ # * **:link** - Find links (`<a>` elements with an href attribute)
89
+ # * Locator: Matches the id, {Capybara.configure test_id}, or title attributes, or the string content of the link,
90
+ # or the alt attribute of a contained img element. By default this selector requires a link to have an href attribute.
91
+ # * Filters:
92
+ # * :title (String) - Matches the title attribute
93
+ # * :alt (String) - Matches the alt attribute of a contained img element
94
+ # * :href (String, Regexp, nil, false) - Matches the normalized href of the link, if nil will find `<a>` elements with no href attribute, if false ignores href presence
95
+ #
96
+ # ```ruby
97
+ # page.html # => '<a href="/">Home</a>'
98
+ #
99
+ # page.find :link, 'Home', href: '/'
100
+ #
101
+ # page.html # => '<a href="/"><img src="/logo.png" alt="The logo"></a>'
102
+ #
103
+ # page.find :link, 'The logo', href: '/'
104
+ # page.find :link, alt: 'The logo', href: '/'
105
+ # ```
106
+ #
107
+ # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
108
+ # * Locator: Matches the id, {Capybara.configure test_id} attribute, name, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
109
+ # * Filters:
110
+ # * :name (String, Regexp) - Matches the name attribute
111
+ # * :title (String) - Matches the title attribute
112
+ # * :value (String) - Matches the value of an input button
113
+ # * :type (String) - Matches the type attribute
114
+ # * :disabled (Boolean, :all) - Match disabled buttons (Default: false)
115
+ #
116
+ # ```ruby
117
+ # page.html # => '<button>Submit</button>'
118
+ #
119
+ # page.find :button, 'Submit'
120
+ #
121
+ # page.html # => '<button name="article[state]" value="draft">Save as draft</button>'
122
+ #
123
+ # page.find :button, 'Save as draft', name: 'article[state]', value: 'draft'
124
+ # ```
125
+ #
126
+ # * **:link_or_button** - Find links or buttons
127
+ # * Locator: See :link and :button selectors
128
+ # * Filters:
129
+ # * :disabled (Boolean, :all) - Match disabled buttons? (Default: false)
130
+ #
131
+ # ```ruby
132
+ # page.html # => '<a href="/">Home</a>'
133
+ #
134
+ # page.find :link_or_button, 'Home'
135
+ #
136
+ # page.html # => '<button>Submit</button>'
137
+ #
138
+ # page.find :link_or_button, 'Submit'
139
+ # ```
140
+ #
141
+ # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
142
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
143
+ # * Filters:
144
+ # * :name (String, Regexp) - Matches the name attribute
145
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
146
+ # * :with (String, Regexp) - Matches the current value of the field
147
+ # * :type (String) - Matches the type attribute of the field or element type for 'textarea'
148
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
149
+ # * :multiple (Boolean) - Match fields that accept multiple values
150
+ # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
151
+ # * :validation_message (String, Regexp) - Matches the elements current validationMessage
152
+ #
153
+ # ```ruby
154
+ # page.html # => '<label for="article_body">Body</label>
155
+ # <textarea id="article_body" name="article[body]"></textarea>'
156
+ #
157
+ # page.find :fillable_field, 'article_body'
158
+ # page.find :fillable_field, 'article[body]'
159
+ # page.find :fillable_field, 'Body'
160
+ # page.find :field, 'Body', type: 'textarea'
161
+ # ```
162
+ #
163
+ # * **:radio_button** - Find radio buttons
164
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
165
+ # * Filters:
166
+ # * :name (String, Regexp) - Matches the name attribute
167
+ # * :checked (Boolean) - Match checked fields?
168
+ # * :unchecked (Boolean) - Match unchecked fields?
169
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
170
+ # * :option (String, Regexp) - Match the current value
171
+ # * :with - Alias of :option
172
+ #
173
+ # ```ruby
174
+ # page.html # => '<input type="radio" id="article_state_published" name="article[state]" value="published" checked>
175
+ # <label for="article_state_published">Published</label>
176
+ # <input type="radio" id="article_state_draft" name="article[state]" value="draft">
177
+ # <label for="article_state_draft">Draft</label>'
178
+ #
179
+ # page.find :radio_button, 'article_state_published'
180
+ # page.find :radio_button, 'article[state]', option: 'published'
181
+ # page.find :radio_button, 'Published', checked: true
182
+ # page.find :radio_button, 'Draft', unchecked: true
183
+ # ```
184
+ #
185
+ # * **:checkbox** - Find checkboxes
186
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
187
+ # * Filters:
188
+ # * :name (String, Regexp) - Matches the name attribute
189
+ # * :checked (Boolean) - Match checked fields?
190
+ # * :unchecked (Boolean) - Match unchecked fields?
191
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
192
+ # * :with (String, Regexp) - Match the current value
193
+ # * :option - Alias of :with
194
+ #
195
+ # ```ruby
196
+ # page.html # => '<input type="checkbox" id="registration_terms" name="registration[terms]" value="true">
197
+ # <label for="registration_terms">I agree to terms and conditions</label>'
198
+ #
199
+ # page.find :checkbox, 'registration_terms'
200
+ # page.find :checkbox, 'registration[terms]'
201
+ # page.find :checkbox, 'I agree to terms and conditions', unchecked: true
202
+ # ```
203
+ #
204
+ # * **:select** - Find select elements
205
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
206
+ # * Filters:
207
+ # * :name (String, Regexp) - Matches the name attribute
208
+ # * :placeholder (String, Placeholder) - Matches the placeholder attribute
209
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
210
+ # * :multiple (Boolean) - Match fields that accept multiple values
211
+ # * :options (Array<String>) - Exact match options
212
+ # * :enabled_options (Array<String>) - Exact match enabled options
213
+ # * :disabled_options (Array<String>) - Exact match disabled options
214
+ # * :with_options (Array<String>) - Partial match options
215
+ # * :selected (String, Array<String>) - Match the selection(s)
216
+ # * :with_selected (String, Array<String>) - Partial match the selection(s)
217
+ #
218
+ # ```ruby
219
+ # page.html # => '<label for="article_category">Category</label>
220
+ # <select id="article_category" name="article[category]">
221
+ # <option value="General" checked></option>
222
+ # <option value="Other"></option>
223
+ # </select>'
224
+ #
225
+ # page.find :select, 'article_category'
226
+ # page.find :select, 'article[category]'
227
+ # page.find :select, 'Category'
228
+ # page.find :select, 'Category', selected: 'General'
229
+ # page.find :select, with_options: ['General']
230
+ # page.find :select, with_options: ['Other']
231
+ # page.find :select, options: ['General', 'Other']
232
+ # page.find :select, options: ['General'] # => raises Capybara::ElementNotFound
233
+ # ```
234
+ #
235
+ # * **:option** - Find option elements
236
+ # * Locator: Match text of option
237
+ # * Filters:
238
+ # * :disabled (Boolean) - Match disabled option
239
+ # * :selected (Boolean) - Match selected option
240
+ #
241
+ # ```ruby
242
+ # page.html # => '<option value="General" checked></option>
243
+ # <option value="Disabled" disabled></option>
244
+ # <option value="Other"></option>'
245
+ #
246
+ # page.find :option, 'General'
247
+ # page.find :option, 'General', selected: true
248
+ # page.find :option, 'Disabled', disabled: true
249
+ # page.find :option, 'Other', selected: false
250
+ # ```
251
+ #
252
+ # * **:datalist_input** - Find input field with datalist completion
253
+ # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name,
254
+ # placeholder, or associated label text
255
+ # * Filters:
256
+ # * :name (String, Regexp) - Matches the name attribute
257
+ # * :placeholder (String, Regexp) - Matches the placeholder attribute
258
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
259
+ # * :options (Array<String>) - Exact match options
260
+ # * :with_options (Array<String>) - Partial match options
261
+ #
262
+ # ```ruby
263
+ # page.html # => '<label for="ice_cream_flavor">Flavor</label>
264
+ # <input list="ice_cream_flavors" id="ice_cream_flavor" name="ice_cream[flavor]">
265
+ # <datalist id="ice_cream_flavors">
266
+ # <option value="Chocolate"></option>
267
+ # <option value="Strawberry"></option>
268
+ # <option value="Vanilla"></option>
269
+ # </datalist>'
270
+ #
271
+ # page.find :datalist_input, 'ice_cream_flavor'
272
+ # page.find :datalist_input, 'ice_cream[flavor]'
273
+ # page.find :datalist_input, 'Flavor'
274
+ # page.find :datalist_input, with_options: ['Chocolate', 'Strawberry']
275
+ # page.find :datalist_input, options: ['Chocolate', 'Strawberry', 'Vanilla']
276
+ # page.find :datalist_input, options: ['Chocolate'] # => raises Capybara::ElementNotFound
277
+ # ```
278
+ #
279
+ # * **:datalist_option** - Find datalist option
280
+ # * Locator: Match text or value of option
281
+ # * Filters:
282
+ # * :disabled (Boolean) - Match disabled option
283
+ #
284
+ # ```ruby
285
+ # page.html # => '<datalist>
286
+ # <option value="Chocolate"></option>
287
+ # <option value="Strawberry"></option>
288
+ # <option value="Vanilla"></option>
289
+ # <option value="Forbidden" disabled></option>
290
+ # </datalist>'
291
+ #
292
+ # page.find :datalist_option, 'Chocolate'
293
+ # page.find :datalist_option, 'Strawberry'
294
+ # page.find :datalist_option, 'Vanilla'
295
+ # page.find :datalist_option, 'Forbidden', disabled: true
296
+ # ```
297
+ #
298
+ # * **:file_field** - Find file input elements
299
+ # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
300
+ # * Filters:
301
+ # * :name (String, Regexp) - Matches the name attribute
302
+ # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
303
+ # * :multiple (Boolean) - Match field that accepts multiple values
304
+ #
305
+ # ```ruby
306
+ # page.html # => '<label for="article_banner_image">Banner Image</label>
307
+ # <input type="file" id="article_banner_image" name="article[banner_image]">'
308
+ #
309
+ # page.find :file_field, 'article_banner_image'
310
+ # page.find :file_field, 'article[banner_image]'
311
+ # page.find :file_field, 'Banner Image'
312
+ # page.find :file_field, 'Banner Image', name: 'article[banner_image]'
313
+ # page.find :field, 'Banner Image', type: 'file'
314
+ # ```
315
+ #
316
+ # * **:label** - Find label elements
317
+ # * Locator: Match id, {Capybara.configure test_id}, or text contents
318
+ # * Filters:
319
+ # * :for (Element, String, Regexp) - The element or id of the element associated with the label
320
+ #
321
+ # ```ruby
322
+ # page.html # => '<label for="article_title">Title</label>
323
+ # <input id="article_title" name="article[title]">'
324
+ #
325
+ # page.find :label, 'Title'
326
+ # page.find :label, 'Title', for: 'article_title'
327
+ # page.find :label, 'Title', for: page.find('article[title]')
328
+ # ```
329
+ #
330
+ # * **:table** - Find table elements
331
+ # * Locator: id, {Capybara.configure test_id}, or caption text of table
332
+ # * Filters:
333
+ # * :caption (String) - Match text of associated caption
334
+ # * :with_rows (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
335
+ # * :rows (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
336
+ # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
337
+ # * :cols (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
338
+ #
339
+ # ```ruby
340
+ # page.html # => '<table>
341
+ # <caption>A table</caption>
342
+ # <tr>
343
+ # <th>A</th>
344
+ # <th>B</th>
345
+ # </tr>
346
+ # <tr>
347
+ # <td>1</td>
348
+ # <td>2</td>
349
+ # </tr>
350
+ # <tr>
351
+ # <td>3</td>
352
+ # <td>4</td>
353
+ # </tr>
354
+ # </table>'
355
+ #
356
+ # page.find :table, 'A table'
357
+ # page.find :table, with_rows: [
358
+ # { 'A' => '1', 'B' => '2' },
359
+ # { 'A' => '3', 'B' => '4' },
360
+ # ]
361
+ # page.find :table, with_rows: [
362
+ # ['1', '2'],
363
+ # ['3', '4'],
364
+ # ]
365
+ # page.find :table, rows: [
366
+ # { 'A' => '1', 'B' => '2' },
367
+ # { 'A' => '3', 'B' => '4' },
368
+ # ]
369
+ # page.find :table, rows: [
370
+ # ['1', '2'],
371
+ # ['3', '4'],
372
+ # ]
373
+ # page.find :table, rows: [ ['1', '2'] ] # => raises Capybara::ElementNotFound
374
+ # ```
375
+ #
376
+ # * **:table_row** - Find table row
377
+ # * Locator: Array<String>, Hash<String, String> table row `<td>` contents - visibility of `<td>` elements is not considered
378
+ #
379
+ # ```ruby
380
+ # page.html # => '<table>
381
+ # <tr>
382
+ # <th>A</th>
383
+ # <th>B</th>
384
+ # </tr>
385
+ # <tr>
386
+ # <td>1</td>
387
+ # <td>2</td>
388
+ # </tr>
389
+ # <tr>
390
+ # <td>3</td>
391
+ # <td>4</td>
392
+ # </tr>
393
+ # </table>'
394
+ #
395
+ # page.find :table_row, 'A' => '1', 'B' => '2'
396
+ # page.find :table_row, 'A' => '3', 'B' => '4'
397
+ # ```
398
+ #
399
+ # * **:frame** - Find frame/iframe elements
400
+ # * Locator: Match id, {Capybara.configure test_id} attribute, or name
401
+ # * Filters:
402
+ # * :name (String) - Match name attribute
403
+ #
404
+ # ```ruby
405
+ # page.html # => '<iframe id="embed_frame" name="embed" src="https://example.com/embed"></iframe>'
406
+ #
407
+ # page.find :frame, 'embed_frame'
408
+ # page.find :frame, 'embed'
409
+ # page.find :frame, name: 'embed'
410
+ # ```
411
+ #
412
+ # * **:element**
413
+ # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
414
+ # * Filters:
415
+ # * :\<any> (String, Regexp) - Match on any specified element attribute
416
+ #
417
+ # ```ruby
418
+ # page.html # => '<button type="button" role="menuitemcheckbox" aria-checked="true">Check me</button>
419
+ #
420
+ # page.find :element, 'button'
421
+ # page.find :element, type: 'button', text: 'Check me'
422
+ # page.find :element, role: 'menuitemcheckbox'
423
+ # page.find :element, role: /checkbox/, 'aria-checked': 'true'
424
+ # ```
425
+ #
426
+ class Capybara::Selector; end # rubocop:disable Lint/EmptyClass
255
427
 
256
- expression_filter(:type) do |expr, type|
257
- type = type.to_s
258
- if ['textarea'].include?(type)
259
- expr.axis(:self, type.to_sym)
260
- else
261
- expr[XPath.attr(:type).equals(type)]
262
- end
263
- end
264
-
265
- filter_set(:_field, [:disabled, :multiple, :name, :placeholder])
266
-
267
- filter(:with) do |node, with|
268
- with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
269
- end
270
-
271
- describe do |options|
272
- desc = String.new
273
- desc << describe_all_expression_filters(options)
274
- desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
275
- desc
276
- end
277
- end
278
-
279
- ##
280
- #
281
- # Find radio buttons
282
- #
283
- # @locator Match id, name, or associated label text
284
- # @filter [String] :id Matches the id attribute
285
- # @filter [String] :name Matches the name attribute
286
- # @filter [String, Array<String>] :class Matches the class(es) provided
287
- # @filter [Boolean] :checked Match checked fields?
288
- # @filter [Boolean] :unchecked Match unchecked fields?
289
- # @filter [Boolean] :disabled Match disabled field?
290
- # @filter [String] :option Match the value
291
- #
292
- Capybara.add_selector(:radio_button) do
293
- label "radio button"
294
- xpath do |locator, options|
295
- xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
296
- locate_field(xpath, locator, options)
297
- end
298
-
299
- filter_set(:_field, [:checked, :unchecked, :disabled, :name])
300
-
301
- filter(:option) { |node, value| node.value == value.to_s }
302
-
303
- describe do |options|
304
- desc = String.new
305
- desc << " with value #{options[:option].inspect}" if options[:option]
306
- desc << describe_all_expression_filters(options)
307
- desc
308
- end
309
- end
310
-
311
- ##
312
- #
313
- # Find checkboxes
314
- #
315
- # @locator Match id, name, or associated label text
316
- # @filter [String] :id Matches the id attribute
317
- # @filter [String] :name Matches the name attribute
318
- # @filter [String, Array<String>] :class Matches the class(es) provided
319
- # @filter [Boolean] :checked Match checked fields?
320
- # @filter [Boolean] :unchecked Match unchecked fields?
321
- # @filter [Boolean] :disabled Match disabled field?
322
- # @filter [String] :option Match the value
323
- #
324
- Capybara.add_selector(:checkbox) do
325
- xpath do |locator, options|
326
- xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
327
- locate_field(xpath, locator, options)
328
- end
329
-
330
- filter_set(:_field, [:checked, :unchecked, :disabled, :name])
331
-
332
- filter(:option) { |node, value| node.value == value.to_s }
333
-
334
- describe do |options|
335
- desc = String.new
336
- desc << " with value #{options[:option].inspect}" if options[:option]
337
- desc << describe_all_expression_filters(options)
338
- desc
339
- end
340
- end
341
-
342
- ##
343
- #
344
- # Find select elements
345
- #
346
- # @locator Match id, name, placeholder, or associated label text
347
- # @filter [String] :id Matches the id attribute
348
- # @filter [String] :name Matches the name attribute
349
- # @filter [String] :placeholder Matches the placeholder attribute
350
- # @filter [String, Array<String>] :class Matches the class(es) provided
351
- # @filter [Boolean] :disabled Match disabled field?
352
- # @filter [Boolean] :multiple Match fields that accept multiple values
353
- # @filter [Array<String>] :options Exact match options
354
- # @filter [Array<String>] :with_options Partial match options
355
- # @filter [String, Array<String>] :selected Match the selection(s)
356
- # @filter [String, Array<String>] :with_selected Partial match the selection(s)
357
- #
358
- Capybara.add_selector(:select) do
359
- label "select box"
360
- xpath do |locator, options|
361
- xpath = XPath.descendant(:select)
362
- locate_field(xpath, locator, options)
363
- end
364
-
365
- filter_set(:_field, [:disabled, :multiple, :name, :placeholder])
366
-
367
- filter(:options) do |node, options|
368
- if node.visible?
369
- actual = node.all(:xpath, './/option').map { |option| option.text }
370
- else
371
- actual = node.all(:xpath, './/option', visible: false).map { |option| option.text(:all) }
372
- end
373
- options.sort == actual.sort
374
- end
375
-
376
- filter(:with_options) do |node, options|
377
- finder_settings = { minimum: 0 }
378
- if !node.visible?
379
- finder_settings[:visible] = false
428
+ Capybara::Selector::FilterSet.add(:_field) do
429
+ node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
430
+ node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
431
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
432
+ node_filter(:valid, :boolean) { |node, value| node.evaluate_script('this.validity.valid') == value }
433
+ node_filter(:name) { |node, value| !value.is_a?(Regexp) || value.match?(node[:name]) }
434
+ node_filter(:placeholder) { |node, value| !value.is_a?(Regexp) || value.match?(node[:placeholder]) }
435
+ node_filter(:validation_message) do |node, msg|
436
+ vm = node[:validationMessage]
437
+ (msg.is_a?(Regexp) ? msg.match?(vm) : vm == msg.to_s).tap do |res|
438
+ add_error("Expected validation message to be #{msg.inspect} but was #{vm}") unless res
380
439
  end
381
- options.all? { |option| node.first(:option, option, finder_settings) }
382
440
  end
383
441
 
384
- filter(:selected) do |node, selected|
385
- actual = node.all(:xpath, './/option', visible: false).select(&:selected?).map { |option| option.text(:all) }
386
- Array(selected).sort == actual.sort
442
+ expression_filter(:name) do |xpath, val|
443
+ builder(xpath).add_attribute_conditions(name: val)
387
444
  end
388
-
389
- filter(:with_selected) do |node, selected|
390
- actual = node.all(:xpath, './/option', visible: false).select(&:selected?).map { |option| option.text(:all) }
391
- (Array(selected) - actual).empty?
445
+ expression_filter(:placeholder) do |xpath, val|
446
+ builder(xpath).add_attribute_conditions(placeholder: val)
392
447
  end
448
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
449
+ expression_filter(:multiple) { |xpath, val| xpath[val ? XPath.attr(:multiple) : ~XPath.attr(:multiple)] }
393
450
 
394
- describe do |options|
395
- desc = String.new
396
- desc << " with options #{options[:options].inspect}" if options[:options]
397
- desc << " with at least options #{options[:with_options].inspect}" if options[:with_options]
398
- desc << " with #{options[:selected].inspect} selected" if options[:selected]
399
- desc << " with at least #{options[:with_selected].inspect} selected" if options[:with_selected]
400
- desc << describe_all_expression_filters(options)
451
+ describe(:expression_filters) do |name: nil, placeholder: nil, disabled: nil, multiple: nil, **|
452
+ desc = +''
453
+ desc << ' that is not disabled' if disabled == false
454
+ desc << " with name #{name}" if name
455
+ desc << " with placeholder #{placeholder}" if placeholder
456
+ desc << ' with the multiple attribute' if multiple == true
457
+ desc << ' without the multiple attribute' if multiple == false
401
458
  desc
402
459
  end
403
- end
404
-
405
- ##
406
- #
407
- # Find option elements
408
- #
409
- # @locator Match text of option
410
- # @filter [Boolean] :disabled Match disabled option
411
- # @filter [Boolean] :selected Match selected option
412
- #
413
- Capybara.add_selector(:option) do
414
- xpath do |locator|
415
- xpath = XPath.descendant(:option)
416
- xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
417
- xpath
418
- end
419
460
 
420
- filter(:disabled, :boolean) { |node, value| not(value ^ node.disabled?) }
421
- filter(:selected, :boolean) { |node, value| not(value ^ node.selected?) }
422
-
423
- describe do |options|
424
- desc = String.new
425
- desc << " that is#{' not' unless options[:disabled]} disabled" if options.has_key?(:disabled)
426
- desc << " that is#{' not' unless options[:selected]} selected" if options.has_key?(:selected)
427
- desc
428
- end
429
- end
430
-
431
- ##
432
- #
433
- # Find file input elements
434
- #
435
- # @locator Match id, name, or associated label text
436
- # @filter [String] :id Matches the id attribute
437
- # @filter [String] :name Matches the name attribute
438
- # @filter [String, Array<String>] :class Matches the class(es) provided
439
- # @filter [Boolean] :disabled Match disabled field?
440
- # @filter [Boolean] :multiple Match field that accepts multiple values
441
- #
442
- Capybara.add_selector(:file_field) do
443
- label "file field"
444
- xpath do |locator, options|
445
- xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
446
- locate_field(xpath, locator, options)
447
- end
448
-
449
- filter_set(:_field, [:disabled, :multiple, :name])
450
-
451
- describe do |options|
452
- desc = String.new
453
- desc << describe_all_expression_filters(options)
454
- desc
455
- end
456
- end
457
-
458
- ##
459
- #
460
- # Find label elements
461
- #
462
- # @locator Match id or text contents
463
- # @filter [Element, String] :for The element or id of the element associated with the label
464
- #
465
- Capybara.add_selector(:label) do
466
- label "label"
467
- xpath(:for) do |locator, options|
468
- xpath = XPath.descendant(:label)
469
- xpath = xpath[XPath.string.n.is(locator.to_s).or XPath.attr(:id).equals(locator.to_s)] unless locator.nil?
470
- if options.has_key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
471
- xpath = xpath[XPath.attr(:for).equals(options[:for].to_s).or((~XPath.attr(:for)).and(XPath.descendant()[XPath.attr(:id).equals(options[:for].to_s)]))]
472
- end
473
- xpath
474
- end
475
-
476
- filter(:for) do |node, field_or_value|
477
- if field_or_value.is_a? Capybara::Node::Element
478
- if node[:for]
479
- field_or_value[:id] == node[:for]
480
- else
481
- field_or_value.find_xpath('./ancestor::label[1]').include? node.base
482
- end
483
- else
484
- #Non element values were handled through the expression filter
485
- true
486
- end
487
- end
488
-
489
- describe do |options|
490
- desc = String.new
491
- desc << " for #{options[:for]}" if options[:for]
492
- desc
493
- end
494
- end
495
-
496
- ##
497
- #
498
- # Find table elements
499
- #
500
- # @locator id or caption text of table
501
- # @filter [String] :id Match id attribute of table
502
- # @filter [String] :caption Match text of associated caption
503
- # @filter [String, Array<String>] :class Matches the class(es) provided
504
- #
505
- Capybara.add_selector(:table) do
506
- xpath(:caption) do |locator, options|
507
- xpath = XPath.descendant(:table)
508
- xpath = xpath[XPath.attr(:id).equals(locator.to_s).or XPath.descendant(:caption).is(locator.to_s)] unless locator.nil?
509
- xpath = xpath[XPath.descendant(:caption).equals(options[:caption])] if options[:caption]
510
- xpath
511
- end
512
-
513
- describe do |options|
514
- desc = String.new
515
- desc << " with caption #{options[:caption]}" if options[:caption]
461
+ describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, valid: nil, validation_message: nil, **|
462
+ desc, states = +'', []
463
+ states << 'checked' if checked || (unchecked == false)
464
+ states << 'not checked' if unchecked || (checked == false)
465
+ states << 'disabled' if disabled == true
466
+ desc << " that is #{states.join(' and ')}" unless states.empty?
467
+ desc << ' that is valid' if valid == true
468
+ desc << ' that is invalid' if valid == false
469
+ desc << " with validation message #{validation_message.to_s.inspect}" if validation_message
516
470
  desc
517
471
  end
518
472
  end
519
473
 
520
- ##
521
- #
522
- # Find frame/iframe elements
523
- #
524
- # @locator Match id or name
525
- # @filter [String] :id Match id attribute
526
- # @filter [String] :name Match name attribute
527
- # @filter [String, Array<String>] :class Matches the class(es) provided
528
- #
529
- Capybara.add_selector(:frame) do
530
- xpath(:name) do |locator, options|
531
- xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
532
- xpath = xpath[XPath.attr(:id).equals(locator.to_s).or XPath.attr(:name).equals(locator)] unless locator.nil?
533
- xpath = expression_filters.keys.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
534
- xpath
535
- end
536
-
537
- describe do |options|
538
- desc = String.new
539
- desc << " with name #{options[:name]}" if options[:name]
540
- desc
541
- end
542
- end
474
+ require 'capybara/selector/definition/xpath'
475
+ require 'capybara/selector/definition/css'
476
+ require 'capybara/selector/definition/id'
477
+ require 'capybara/selector/definition/field'
478
+ require 'capybara/selector/definition/fieldset'
479
+ require 'capybara/selector/definition/link'
480
+ require 'capybara/selector/definition/button'
481
+ require 'capybara/selector/definition/link_or_button'
482
+ require 'capybara/selector/definition/fillable_field'
483
+ require 'capybara/selector/definition/radio_button'
484
+ require 'capybara/selector/definition/checkbox'
485
+ require 'capybara/selector/definition/select'
486
+ require 'capybara/selector/definition/datalist_input'
487
+ require 'capybara/selector/definition/option'
488
+ require 'capybara/selector/definition/datalist_option'
489
+ require 'capybara/selector/definition/file_field'
490
+ require 'capybara/selector/definition/label'
491
+ require 'capybara/selector/definition/table'
492
+ require 'capybara/selector/definition/table_row'
493
+ require 'capybara/selector/definition/frame'
494
+ require 'capybara/selector/definition/element'