capybara 3.35.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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +168 -5
  3. data/README.md +199 -39
  4. data/lib/capybara/config.rb +16 -4
  5. data/lib/capybara/driver/base.rb +4 -0
  6. data/lib/capybara/driver/node.rb +5 -1
  7. data/lib/capybara/dsl.rb +4 -10
  8. data/lib/capybara/helpers.rb +9 -14
  9. data/lib/capybara/minitest/spec.rb +18 -6
  10. data/lib/capybara/minitest.rb +14 -1
  11. data/lib/capybara/node/actions.rb +14 -9
  12. data/lib/capybara/node/base.rb +2 -1
  13. data/lib/capybara/node/document.rb +2 -2
  14. data/lib/capybara/node/element.rb +13 -2
  15. data/lib/capybara/node/finders.rb +11 -2
  16. data/lib/capybara/node/matchers.rb +25 -0
  17. data/lib/capybara/node/simple.rb +5 -1
  18. data/lib/capybara/node/whitespace_normalizer.rb +81 -0
  19. data/lib/capybara/queries/active_element_query.rb +18 -0
  20. data/lib/capybara/queries/ancestor_query.rb +2 -1
  21. data/lib/capybara/queries/base_query.rb +2 -2
  22. data/lib/capybara/queries/current_path_query.rb +1 -1
  23. data/lib/capybara/queries/selector_query.rb +40 -11
  24. data/lib/capybara/queries/sibling_query.rb +2 -1
  25. data/lib/capybara/queries/text_query.rb +1 -1
  26. data/lib/capybara/rack_test/browser.rb +64 -8
  27. data/lib/capybara/rack_test/driver.rb +4 -4
  28. data/lib/capybara/rack_test/form.rb +29 -7
  29. data/lib/capybara/rack_test/node.rb +32 -33
  30. data/lib/capybara/registration_container.rb +2 -5
  31. data/lib/capybara/registrations/drivers.rb +7 -7
  32. data/lib/capybara/registrations/servers.rb +37 -16
  33. data/lib/capybara/result.rb +2 -2
  34. data/lib/capybara/rspec/matcher_proxies.rb +6 -6
  35. data/lib/capybara/rspec/matchers/base.rb +8 -6
  36. data/lib/capybara/rspec/matchers/compound.rb +1 -1
  37. data/lib/capybara/rspec/matchers/have_selector.rb +9 -17
  38. data/lib/capybara/rspec/matchers.rb +21 -16
  39. data/lib/capybara/selector/builders/css_builder.rb +1 -1
  40. data/lib/capybara/selector/builders/xpath_builder.rb +1 -1
  41. data/lib/capybara/selector/css.rb +6 -6
  42. data/lib/capybara/selector/definition/button.rb +10 -5
  43. data/lib/capybara/selector/definition/checkbox.rb +1 -1
  44. data/lib/capybara/selector/definition/file_field.rb +1 -1
  45. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  46. data/lib/capybara/selector/definition/link.rb +2 -1
  47. data/lib/capybara/selector/definition/radio_button.rb +1 -1
  48. data/lib/capybara/selector/definition/table.rb +1 -1
  49. data/lib/capybara/selector/definition/table_row.rb +2 -2
  50. data/lib/capybara/selector/definition.rb +4 -2
  51. data/lib/capybara/selector/filter_set.rb +4 -7
  52. data/lib/capybara/selector/regexp_disassembler.rb +2 -5
  53. data/lib/capybara/selector/selector.rb +5 -1
  54. data/lib/capybara/selector.rb +252 -0
  55. data/lib/capybara/selenium/driver.rb +31 -54
  56. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  57. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +9 -5
  58. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -7
  59. data/lib/capybara/selenium/extensions/html5_drag.rb +5 -4
  60. data/lib/capybara/selenium/node.rb +60 -38
  61. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -16
  62. data/lib/capybara/selenium/nodes/edge_node.rb +19 -13
  63. data/lib/capybara/selenium/nodes/firefox_node.rb +3 -3
  64. data/lib/capybara/selenium/nodes/safari_node.rb +4 -4
  65. data/lib/capybara/selenium/patches/atoms.rb +1 -1
  66. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -1
  67. data/lib/capybara/server/animation_disabler.rb +40 -23
  68. data/lib/capybara/server/middleware.rb +1 -1
  69. data/lib/capybara/server.rb +1 -1
  70. data/lib/capybara/session/config.rb +4 -2
  71. data/lib/capybara/session.rb +34 -34
  72. data/lib/capybara/spec/public/test.js +4 -0
  73. data/lib/capybara/spec/session/active_element_spec.rb +31 -0
  74. data/lib/capybara/spec/session/all_spec.rb +11 -15
  75. data/lib/capybara/spec/session/assert_text_spec.rb +17 -17
  76. data/lib/capybara/spec/session/attach_file_spec.rb +6 -0
  77. data/lib/capybara/spec/session/check_spec.rb +10 -0
  78. data/lib/capybara/spec/session/choose_spec.rb +6 -0
  79. data/lib/capybara/spec/session/click_link_or_button_spec.rb +9 -0
  80. data/lib/capybara/spec/session/click_link_spec.rb +12 -1
  81. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  82. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  83. data/lib/capybara/spec/session/find_link_spec.rb +10 -0
  84. data/lib/capybara/spec/session/find_spec.rb +15 -1
  85. data/lib/capybara/spec/session/first_spec.rb +1 -1
  86. data/lib/capybara/spec/session/frame/within_frame_spec.rb +2 -0
  87. data/lib/capybara/spec/session/has_all_selectors_spec.rb +5 -5
  88. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -2
  89. data/lib/capybara/spec/session/has_any_selectors_spec.rb +6 -2
  90. data/lib/capybara/spec/session/has_button_spec.rb +30 -0
  91. data/lib/capybara/spec/session/has_current_path_spec.rb +3 -3
  92. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  93. data/lib/capybara/spec/session/has_field_spec.rb +25 -1
  94. data/lib/capybara/spec/session/has_link_spec.rb +40 -0
  95. data/lib/capybara/spec/session/has_none_selectors_spec.rb +7 -7
  96. data/lib/capybara/spec/session/has_select_spec.rb +10 -4
  97. data/lib/capybara/spec/session/has_selector_spec.rb +15 -0
  98. data/lib/capybara/spec/session/has_table_spec.rb +13 -2
  99. data/lib/capybara/spec/session/has_text_spec.rb +6 -14
  100. data/lib/capybara/spec/session/matches_style_spec.rb +2 -0
  101. data/lib/capybara/spec/session/node_spec.rb +88 -1
  102. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  103. data/lib/capybara/spec/session/reset_session_spec.rb +13 -0
  104. data/lib/capybara/spec/session/scroll_spec.rb +7 -5
  105. data/lib/capybara/spec/session/uncheck_spec.rb +1 -1
  106. data/lib/capybara/spec/session/visit_spec.rb +20 -0
  107. data/lib/capybara/spec/session/window/window_spec.rb +1 -1
  108. data/lib/capybara/spec/session/window/windows_spec.rb +1 -1
  109. data/lib/capybara/spec/session/within_spec.rb +13 -0
  110. data/lib/capybara/spec/spec_helper.rb +12 -5
  111. data/lib/capybara/spec/test_app.rb +91 -14
  112. data/lib/capybara/spec/views/animated.erb +1 -1
  113. data/lib/capybara/spec/views/form.erb +34 -4
  114. data/lib/capybara/spec/views/frame_child.erb +1 -1
  115. data/lib/capybara/spec/views/frame_one.erb +1 -1
  116. data/lib/capybara/spec/views/frame_parent.erb +1 -1
  117. data/lib/capybara/spec/views/frame_two.erb +1 -1
  118. data/lib/capybara/spec/views/initial_alert.erb +2 -1
  119. data/lib/capybara/spec/views/layout.erb +10 -0
  120. data/lib/capybara/spec/views/obscured.erb +1 -1
  121. data/lib/capybara/spec/views/offset.erb +2 -1
  122. data/lib/capybara/spec/views/path.erb +2 -2
  123. data/lib/capybara/spec/views/popup_one.erb +1 -1
  124. data/lib/capybara/spec/views/popup_two.erb +1 -1
  125. data/lib/capybara/spec/views/react.erb +2 -2
  126. data/lib/capybara/spec/views/scroll.erb +2 -1
  127. data/lib/capybara/spec/views/spatial.erb +1 -1
  128. data/lib/capybara/spec/views/with_animation.erb +2 -3
  129. data/lib/capybara/spec/views/with_base_tag.erb +2 -2
  130. data/lib/capybara/spec/views/with_dragula.erb +2 -2
  131. data/lib/capybara/spec/views/with_fixed_header_footer.erb +2 -1
  132. data/lib/capybara/spec/views/with_hover.erb +2 -2
  133. data/lib/capybara/spec/views/with_html.erb +5 -3
  134. data/lib/capybara/spec/views/with_jquery_animation.erb +1 -1
  135. data/lib/capybara/spec/views/with_js.erb +2 -3
  136. data/lib/capybara/spec/views/with_jstree.erb +1 -1
  137. data/lib/capybara/spec/views/with_namespace.erb +1 -0
  138. data/lib/capybara/spec/views/with_scope.erb +2 -2
  139. data/lib/capybara/spec/views/with_shadow.erb +31 -0
  140. data/lib/capybara/spec/views/with_slow_unload.erb +2 -1
  141. data/lib/capybara/spec/views/with_sortable_js.erb +2 -2
  142. data/lib/capybara/spec/views/with_unload_alert.erb +1 -0
  143. data/lib/capybara/spec/views/with_windows.erb +1 -1
  144. data/lib/capybara/spec/views/within_frames.erb +1 -1
  145. data/lib/capybara/version.rb +1 -1
  146. data/lib/capybara/window.rb +1 -1
  147. data/lib/capybara.rb +30 -30
  148. data/spec/basic_node_spec.rb +16 -3
  149. data/spec/capybara_spec.rb +12 -0
  150. data/spec/counter_spec.rb +35 -0
  151. data/spec/css_builder_spec.rb +1 -1
  152. data/spec/css_splitter_spec.rb +1 -1
  153. data/spec/dsl_spec.rb +5 -3
  154. data/spec/fixtures/selenium_driver_rspec_failure.rb +2 -2
  155. data/spec/fixtures/selenium_driver_rspec_success.rb +2 -2
  156. data/spec/minitest_spec.rb +12 -1
  157. data/spec/minitest_spec_spec.rb +4 -0
  158. data/spec/per_session_config_spec.rb +1 -1
  159. data/spec/rack_test_spec.rb +30 -12
  160. data/spec/result_spec.rb +41 -35
  161. data/spec/rspec/features_spec.rb +3 -3
  162. data/spec/rspec/scenarios_spec.rb +2 -2
  163. data/spec/rspec/shared_spec_matchers.rb +27 -3
  164. data/spec/rspec_matchers_spec.rb +25 -0
  165. data/spec/rspec_spec.rb +3 -3
  166. data/spec/sauce_spec_chrome.rb +5 -5
  167. data/spec/selector_spec.rb +4 -4
  168. data/spec/selenium_spec_chrome.rb +20 -18
  169. data/spec/selenium_spec_chrome_remote.rb +15 -19
  170. data/spec/selenium_spec_edge.rb +19 -6
  171. data/spec/selenium_spec_firefox.rb +26 -8
  172. data/spec/selenium_spec_firefox_remote.rb +18 -4
  173. data/spec/selenium_spec_ie.rb +7 -8
  174. data/spec/selenium_spec_safari.rb +34 -20
  175. data/spec/server_spec.rb +19 -7
  176. data/spec/shared_selenium_node.rb +0 -4
  177. data/spec/shared_selenium_session.rb +22 -14
  178. data/spec/spec_helper.rb +36 -3
  179. data/spec/whitespace_normalizer_spec.rb +54 -0
  180. data/spec/xpath_builder_spec.rb +1 -1
  181. metadata +49 -30
  182. data/lib/capybara/selenium/logger_suppressor.rb +0 -34
  183. data/lib/capybara/selenium/patches/action_pauser.rb +0 -26
  184. data/lib/capybara/spec/views/with_title.erb +0 -5
@@ -14,18 +14,37 @@ require 'capybara/selector/definition'
14
14
  # * :left_of (Element) - Match elements left of the passed element on the page
15
15
  # * :right_of (Element) - Match elements right of the passed element on the page
16
16
  # * :near (Element) - Match elements near (within 50px) the passed element on the page
17
+ # * :focused (Boolean) - Match elements with focus (requires driver support)
17
18
  #
18
19
  # ### Built-in Selectors
19
20
  #
20
21
  # * **:xpath** - Select elements by XPath expression
21
22
  # * Locator: An XPath expression
22
23
  #
24
+ # ```ruby
25
+ # page.html # => '<input>'
26
+ #
27
+ # page.find :xpath, './/input'
28
+ # ```
29
+ #
23
30
  # * **:css** - Select elements by CSS selector
24
31
  # * Locator: A CSS selector
25
32
  #
33
+ # ```ruby
34
+ # page.html # => '<input>'
35
+ #
36
+ # page.find :css, 'input'
37
+ # ```
38
+ #
26
39
  # * **:id** - Select element by id
27
40
  # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
28
41
  #
42
+ # ```ruby
43
+ # page.html # => '<input id="field">'
44
+ #
45
+ # page.find :id, 'content'
46
+ # ```
47
+ #
29
48
  # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
30
49
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or
31
50
  # associated label text
@@ -42,12 +61,30 @@ require 'capybara/selector/definition'
42
61
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
43
62
  # * :validation_message (String, Regexp) - Matches the elements current validationMessage
44
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
+ #
45
74
  # * **:fieldset** - Select fieldset elements
46
75
  # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
47
76
  # * Filters:
48
77
  # * :legend (String) - Matches contents of wrapped legend
49
78
  # * :disabled (Boolean) - Match disabled fieldset?
50
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
+ #
51
88
  # * **:link** - Find links (`<a>` elements with an href attribute)
52
89
  # * Locator: Matches the id, {Capybara.configure test_id}, or title attributes, or the string content of the link,
53
90
  # or the alt attribute of a contained img element. By default this selector requires a link to have an href attribute.
@@ -56,6 +93,17 @@ require 'capybara/selector/definition'
56
93
  # * :alt (String) - Matches the alt attribute of a contained img element
57
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
58
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
+ #
59
107
  # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
60
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
61
109
  # * Filters:
@@ -65,11 +113,31 @@ require 'capybara/selector/definition'
65
113
  # * :type (String) - Matches the type attribute
66
114
  # * :disabled (Boolean, :all) - Match disabled buttons (Default: false)
67
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
+ #
68
126
  # * **:link_or_button** - Find links or buttons
69
127
  # * Locator: See :link and :button selectors
70
128
  # * Filters:
71
129
  # * :disabled (Boolean, :all) - Match disabled buttons? (Default: false)
72
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
+ #
73
141
  # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
74
142
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
75
143
  # * Filters:
@@ -82,6 +150,16 @@ require 'capybara/selector/definition'
82
150
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
83
151
  # * :validation_message (String, Regexp) - Matches the elements current validationMessage
84
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
+ #
85
163
  # * **:radio_button** - Find radio buttons
86
164
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
87
165
  # * Filters:
@@ -92,6 +170,18 @@ require 'capybara/selector/definition'
92
170
  # * :option (String, Regexp) - Match the current value
93
171
  # * :with - Alias of :option
94
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
+ #
95
185
  # * **:checkbox** - Find checkboxes
96
186
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
97
187
  # * Filters:
@@ -102,6 +192,15 @@ require 'capybara/selector/definition'
102
192
  # * :with (String, Regexp) - Match the current value
103
193
  # * :option - Alias of :with
104
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
+ #
105
204
  # * **:select** - Find select elements
106
205
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
107
206
  # * Filters:
@@ -116,12 +215,40 @@ require 'capybara/selector/definition'
116
215
  # * :selected (String, Array<String>) - Match the selection(s)
117
216
  # * :with_selected (String, Array<String>) - Partial match the selection(s)
118
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
+ #
119
235
  # * **:option** - Find option elements
120
236
  # * Locator: Match text of option
121
237
  # * Filters:
122
238
  # * :disabled (Boolean) - Match disabled option
123
239
  # * :selected (Boolean) - Match selected option
124
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
+ #
125
252
  # * **:datalist_input** - Find input field with datalist completion
126
253
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name,
127
254
  # placeholder, or associated label text
@@ -132,11 +259,42 @@ require 'capybara/selector/definition'
132
259
  # * :options (Array<String>) - Exact match options
133
260
  # * :with_options (Array<String>) - Partial match options
134
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
+ #
135
279
  # * **:datalist_option** - Find datalist option
136
280
  # * Locator: Match text or value of option
137
281
  # * Filters:
138
282
  # * :disabled (Boolean) - Match disabled option
139
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
+ #
140
298
  # * **:file_field** - Find file input elements
141
299
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
142
300
  # * Filters:
@@ -144,11 +302,31 @@ require 'capybara/selector/definition'
144
302
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
145
303
  # * :multiple (Boolean) - Match field that accepts multiple values
146
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
+ #
147
316
  # * **:label** - Find label elements
148
317
  # * Locator: Match id, {Capybara.configure test_id}, or text contents
149
318
  # * Filters:
150
319
  # * :for (Element, String, Regexp) - The element or id of the element associated with the label
151
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
+ #
152
330
  # * **:table** - Find table elements
153
331
  # * Locator: id, {Capybara.configure test_id}, or caption text of table
154
332
  # * Filters:
@@ -158,19 +336,93 @@ require 'capybara/selector/definition'
158
336
  # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
159
337
  # * :cols (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
160
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
+ #
161
376
  # * **:table_row** - Find table row
162
377
  # * Locator: Array<String>, Hash<String, String> table row `<td>` contents - visibility of `<td>` elements is not considered
163
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
+ #
164
399
  # * **:frame** - Find frame/iframe elements
165
400
  # * Locator: Match id, {Capybara.configure test_id} attribute, or name
166
401
  # * Filters:
167
402
  # * :name (String) - Match name attribute
168
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
+ #
169
412
  # * **:element**
170
413
  # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
171
414
  # * Filters:
172
415
  # * :\<any> (String, Regexp) - Match on any specified element attribute
173
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
+ #
174
426
  class Capybara::Selector; end # rubocop:disable Lint/EmptyClass
175
427
 
176
428
  Capybara::Selector::FilterSet.add(:_field) do
@@ -12,7 +12,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
12
12
  clear_session_storage: nil
13
13
  }.freeze
14
14
  SPECIAL_OPTIONS = %i[browser clear_local_storage clear_session_storage timeout native_displayed].freeze
15
- CAPS_VERSION = Gem::Requirement.new('~> 4.0.0.alpha6')
15
+ CAPS_VERSION = Gem::Requirement.new('< 4.8.0')
16
16
 
17
17
  attr_reader :app, :options
18
18
 
@@ -21,10 +21,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
21
21
 
22
22
  def load_selenium
23
23
  require 'selenium-webdriver'
24
- require 'capybara/selenium/logger_suppressor'
25
24
  require 'capybara/selenium/patches/atoms'
26
25
  require 'capybara/selenium/patches/is_displayed'
27
- require 'capybara/selenium/patches/action_pauser'
28
26
 
29
27
  # Look up the version of `selenium-webdriver` to
30
28
  # see if it's a version we support.
@@ -43,8 +41,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
43
41
  Gem::Version.new(Selenium::WebDriver::VERSION)
44
42
  end
45
43
 
46
- unless Gem::Requirement.new('>= 3.5.0').satisfied_by? @selenium_webdriver_version
47
- warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade."
44
+ unless Gem::Requirement.new('>= 4.8').satisfied_by? @selenium_webdriver_version
45
+ warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade to 4.8+."
48
46
  end
49
47
 
50
48
  @selenium_webdriver_version
@@ -72,16 +70,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
72
70
  ::Capybara::Selenium::PersistentClient.new
73
71
  end
74
72
  end
75
- processed_options = options.reject { |key, _val| SPECIAL_OPTIONS.include?(key) }
76
-
77
- @browser = if options[:browser] == :firefox &&
78
- RUBY_VERSION >= '3.0' &&
79
- Capybara::Selenium::Driver.selenium_webdriver_version <= Gem::Version.new('4.0.0.alpha1')
80
- # selenium-webdriver 3.x doesn't correctly pass options through for Firefox with Ruby 3 so workaround that
81
- Selenium::WebDriver::Firefox::Driver.new(**processed_options)
82
- else
83
- Selenium::WebDriver.for(options[:browser], processed_options)
84
- end
73
+ processed_options = options.except(*SPECIAL_OPTIONS)
74
+
75
+ @browser = Selenium::WebDriver.for(options[:browser], processed_options)
85
76
 
86
77
  specialize_driver
87
78
  setup_exit_handler
@@ -148,12 +139,17 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
148
139
  unwrap_script_result(result)
149
140
  end
150
141
 
142
+ def active_element
143
+ build_node(native_active_element)
144
+ end
145
+
151
146
  def send_keys(*args)
152
- active_element.send_keys(*args)
147
+ # Should this call the specialized nodes rather than native???
148
+ native_active_element.send_keys(*args)
153
149
  end
154
150
 
155
- def save_screenshot(path, **_options)
156
- browser.save_screenshot(path)
151
+ def save_screenshot(path, **options)
152
+ browser.save_screenshot(path, **options)
157
153
  end
158
154
 
159
155
  def reset!
@@ -249,7 +245,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
249
245
  end
250
246
 
251
247
  def open_new_window(kind = :tab)
252
- browser.manage.new_window(kind)
248
+ if browser.switch_to.respond_to?(:new_window)
249
+ handle = current_window_handle
250
+ browser.switch_to.new_window(kind)
251
+ switch_to_window(handle)
252
+ else
253
+ browser.manage.new_window(kind)
254
+ end
253
255
  rescue NoMethodError, Selenium::WebDriver::Error::WebDriverError
254
256
  # If not supported by the driver or browser default to using JS
255
257
  browser.execute_script('window.open();')
@@ -293,7 +295,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
293
295
  end
294
296
 
295
297
  def invalid_element_errors
296
- @invalid_element_errors ||= begin
298
+ @invalid_element_errors ||=
297
299
  [
298
300
  ::Selenium::WebDriver::Error::StaleElementReferenceError,
299
301
  ::Selenium::WebDriver::Error::ElementNotInteractableError,
@@ -302,18 +304,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
302
304
  ::Selenium::WebDriver::Error::NoSuchElementError, # IE
303
305
  ::Selenium::WebDriver::Error::InvalidArgumentError # IE
304
306
  ].tap do |errors|
305
- unless selenium_4?
306
- ::Selenium::WebDriver.logger.suppress_deprecations do
307
- errors.concat [
308
- ::Selenium::WebDriver::Error::UnhandledError,
309
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
310
- ::Selenium::WebDriver::Error::InvalidElementStateError,
311
- ::Selenium::WebDriver::Error::ElementNotSelectableError
312
- ]
313
- end
307
+ if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
308
+ errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
314
309
  end
315
310
  end
316
- end
317
311
  end
318
312
 
319
313
  def no_such_window_error
@@ -322,14 +316,14 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
322
316
 
323
317
  private
324
318
 
325
- def selenium_4?
326
- defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
327
- end
328
-
329
319
  def native_args(args)
330
320
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
331
321
  end
332
322
 
323
+ def native_active_element
324
+ browser.switch_to.active_element
325
+ end
326
+
333
327
  def clear_browser_state
334
328
  delete_all_cookies
335
329
  clear_storage
@@ -344,10 +338,7 @@ private
344
338
  end
345
339
 
346
340
  def unhandled_alert_errors
347
- @unhandled_alert_errors ||= with_legacy_error(
348
- [Selenium::WebDriver::Error::UnexpectedAlertOpenError],
349
- 'UnhandledAlertError'
350
- )
341
+ @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError]
351
342
  end
352
343
 
353
344
  def delete_all_cookies
@@ -437,17 +428,7 @@ private
437
428
  end
438
429
 
439
430
  def find_modal_errors
440
- @find_modal_errors ||= with_legacy_error([Selenium::WebDriver::Error::TimeoutError], 'TimeOutError')
441
- end
442
-
443
- def with_legacy_error(errors, legacy_error)
444
- errors.tap do |errs|
445
- unless selenium_4?
446
- ::Selenium::WebDriver.logger.suppress_deprecations do
447
- errs << Selenium::WebDriver::Error.const_get(legacy_error)
448
- end
449
- end
450
- end
431
+ @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError]
451
432
  end
452
433
 
453
434
  def silenced_unknown_error_message?(msg)
@@ -464,7 +445,7 @@ private
464
445
  arg.map { |arr| unwrap_script_result(arr) }
465
446
  when Hash
466
447
  arg.transform_values! { |value| unwrap_script_result(value) }
467
- when Selenium::WebDriver::Element
448
+ when Selenium::WebDriver::Element, Selenium::WebDriver::ShadowRoot
468
449
  build_node(arg)
469
450
  else
470
451
  arg
@@ -475,10 +456,6 @@ private
475
456
  browser
476
457
  end
477
458
 
478
- def active_element
479
- browser.switch_to.active_element
480
- end
481
-
482
459
  def build_node(native_node, initial_cache = {})
483
460
  ::Capybara::Selenium::Node.new(self, native_node, initial_cache)
484
461
  end
@@ -38,7 +38,7 @@ module Capybara::Selenium::Driver::ChromeDriver
38
38
  return unless @browser
39
39
 
40
40
  switch_to_window(window_handles.first)
41
- window_handles.slice(1..-1).each { |win| close_window(win) }
41
+ window_handles.slice(1..).each { |win| close_window(win) }
42
42
  return super if chromedriver_version < 73
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
@@ -39,7 +39,7 @@ module Capybara::Selenium::Driver::EdgeDriver
39
39
  return unless @browser
40
40
 
41
41
  switch_to_window(window_handles.first)
42
- window_handles.slice(1..-1).each { |win| close_window(win) }
42
+ window_handles.slice(1..).each { |win| close_window(win) }
43
43
 
44
44
  timer = Capybara::Helpers.timer(expire_in: 10)
45
45
  begin
@@ -103,9 +103,13 @@ private
103
103
  end
104
104
 
105
105
  def execute_cdp(cmd, params = {})
106
- args = { cmd: cmd, params: params }
107
- result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args)
108
- result['value']
106
+ if browser.respond_to? :execute_cdp
107
+ browser.execute_cdp(cmd, **params)
108
+ else
109
+ args = { cmd: cmd, params: params }
110
+ result = bridge.http.call(:post, "session/#{bridge.session_id}/ms/cdp/execute", args)
111
+ result['value']
112
+ end
109
113
  end
110
114
 
111
115
  def build_node(native_node, initial_cache = {})
@@ -115,7 +119,7 @@ private
115
119
  def edgedriver_version
116
120
  @edgedriver_version ||= begin
117
121
  caps = browser.capabilities
118
- caps['chrome']&.fetch('chromedriverVersion', nil).to_f
122
+ caps['msedge']&.fetch('msedgedriverVersion', nil).to_f
119
123
  end
120
124
  end
121
125
  end
@@ -4,15 +4,10 @@ require 'capybara/selenium/nodes/firefox_node'
4
4
 
5
5
  module Capybara::Selenium::Driver::FirefoxDriver
6
6
  def self.extended(driver)
7
- driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver if w3c?(driver)
7
+ driver.extend Capybara::Selenium::Driver::W3CFirefoxDriver
8
8
  bridge = driver.send(:bridge)
9
9
  bridge.extend Capybara::Selenium::IsDisplayed unless bridge.send(:commands, :is_element_displayed)
10
10
  end
11
-
12
- def self.w3c?(driver)
13
- (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
14
- driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
15
- end
16
11
  end
17
12
 
18
13
  module Capybara::Selenium::Driver::W3CFirefoxDriver
@@ -52,7 +47,7 @@ module Capybara::Selenium::Driver::W3CFirefoxDriver
52
47
  end
53
48
 
54
49
  switch_to_window(window_handles.first)
55
- window_handles.slice(1..-1).each { |win| close_window(win) }
50
+ window_handles.slice(1..).each { |win| close_window(win) }
56
51
  super
57
52
  end
58
53
 
@@ -39,10 +39,8 @@ class Capybara::Selenium::Node
39
39
  input.set_file(args)
40
40
  driver.execute_script DROP_FILE, self, input
41
41
  else
42
- items = args.each_with_object([]) do |arg, arr|
43
- arg.each_with_object(arr) do |(type, data), arr_|
44
- arr_ << { type: type, data: data }
45
- end
42
+ items = args.flat_map do |arg|
43
+ arg.map { |(type, data)| { type: type, data: data } }
46
44
  end
47
45
  driver.execute_script DROP_STRING, items, self
48
46
  end
@@ -168,6 +166,9 @@ class Capybara::Selenium::Node
168
166
  opts[key + 'Key'] = true;
169
167
  }
170
168
 
169
+ var dragEnterEvent = new DragEvent('dragenter', opts);
170
+ target.dispatchEvent(dragEnterEvent);
171
+
171
172
  // fire 2 dragover events to simulate dragging with a direction
172
173
  var entryPoint = pointOnRect(sourceCenter, targetRect)
173
174
  var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);