capybara 3.39.2 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +22 -0
  3. data/README.md +173 -29
  4. data/lib/capybara/helpers.rb +1 -1
  5. data/lib/capybara/minitest/spec.rb +16 -4
  6. data/lib/capybara/minitest.rb +14 -1
  7. data/lib/capybara/node/matchers.rb +25 -0
  8. data/lib/capybara/node/whitespace_normalizer.rb +5 -5
  9. data/lib/capybara/queries/selector_query.rb +2 -1
  10. data/lib/capybara/rack_test/browser.rb +3 -2
  11. data/lib/capybara/rack_test/node.rb +5 -12
  12. data/lib/capybara/registration_container.rb +2 -2
  13. data/lib/capybara/registrations/drivers.rb +1 -1
  14. data/lib/capybara/registrations/servers.rb +8 -7
  15. data/lib/capybara/result.rb +2 -2
  16. data/lib/capybara/rspec/matchers/have_selector.rb +4 -12
  17. data/lib/capybara/rspec/matchers.rb +7 -2
  18. data/lib/capybara/selector/css.rb +5 -5
  19. data/lib/capybara/selector/definition/table.rb +1 -1
  20. data/lib/capybara/selector/definition/table_row.rb +2 -2
  21. data/lib/capybara/selector.rb +251 -0
  22. data/lib/capybara/selenium/driver.rb +11 -51
  23. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +1 -6
  24. data/lib/capybara/selenium/node.rb +4 -31
  25. data/lib/capybara/selenium/nodes/chrome_node.rb +3 -19
  26. data/lib/capybara/selenium/nodes/edge_node.rb +0 -16
  27. data/lib/capybara/server/animation_disabler.rb +1 -1
  28. data/lib/capybara/server.rb +1 -1
  29. data/lib/capybara/session.rb +3 -2
  30. data/lib/capybara/spec/session/all_spec.rb +1 -1
  31. data/lib/capybara/spec/session/click_link_spec.rb +1 -1
  32. data/lib/capybara/spec/session/find_spec.rb +8 -0
  33. data/lib/capybara/spec/session/has_element_spec.rb +47 -0
  34. data/lib/capybara/spec/session/has_table_spec.rb +13 -2
  35. data/lib/capybara/spec/session/node_spec.rb +6 -0
  36. data/lib/capybara/spec/session/node_wrapper_spec.rb +1 -1
  37. data/lib/capybara/spec/session/uncheck_spec.rb +1 -1
  38. data/lib/capybara/spec/spec_helper.rb +2 -2
  39. data/lib/capybara/spec/test_app.rb +1 -1
  40. data/lib/capybara/spec/views/form.erb +5 -0
  41. data/lib/capybara/spec/views/with_html.erb +2 -0
  42. data/lib/capybara/version.rb +1 -1
  43. data/lib/capybara.rb +7 -6
  44. data/spec/minitest_spec.rb +8 -1
  45. data/spec/result_spec.rb +9 -0
  46. data/spec/rspec/shared_spec_matchers.rb +26 -2
  47. data/spec/rspec_spec.rb +1 -1
  48. data/spec/sauce_spec_chrome.rb +1 -1
  49. data/spec/selector_spec.rb +1 -1
  50. data/spec/selenium_spec_chrome.rb +7 -5
  51. data/spec/selenium_spec_chrome_remote.rb +0 -5
  52. data/spec/selenium_spec_edge.rb +11 -4
  53. data/spec/selenium_spec_firefox.rb +4 -3
  54. data/spec/selenium_spec_firefox_remote.rb +2 -3
  55. data/spec/server_spec.rb +12 -0
  56. data/spec/shared_selenium_session.rb +0 -2
  57. data/spec/spec_helper.rb +2 -2
  58. metadata +25 -32
  59. data/lib/capybara/selenium/logger_suppressor.rb +0 -44
  60. data/lib/capybara/selenium/patches/action_pauser.rb +0 -26
@@ -77,12 +77,17 @@ module Capybara
77
77
  #
78
78
  # @see Capybara::Node::Matchers#matches_css?
79
79
 
80
- %i[link button field select table].each do |selector|
80
+ %i[link button field select table element].each do |selector|
81
81
  define_method "have_#{selector}" do |locator = nil, **options, &optional_filter_block|
82
82
  Matchers::HaveSelector.new(selector, locator, **options, &optional_filter_block)
83
83
  end
84
84
  end
85
85
 
86
+ # @!method have_element(locator = nil, **options, &optional_filter_block)
87
+ # RSpec matcher for elements.
88
+ #
89
+ # @see Capybara::Node::Matchers#has_element?
90
+
86
91
  # @!method have_link(locator = nil, **options, &optional_filter_block)
87
92
  # RSpec matcher for links.
88
93
  #
@@ -161,7 +166,7 @@ module Capybara
161
166
 
162
167
  %w[selector css xpath text title current_path link button
163
168
  field checked_field unchecked_field select table
164
- sibling ancestor].each do |matcher_type|
169
+ sibling ancestor element].each do |matcher_type|
165
170
  define_method "have_no_#{matcher_type}" do |*args, **kw_args, &optional_filter_block|
166
171
  Matchers::NegatedMatcher.new(send("have_#{matcher_type}", *args, **kw_args, &optional_filter_block))
167
172
  end
@@ -23,11 +23,11 @@ module Capybara
23
23
  end
24
24
 
25
25
  S = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}'
26
- H = /[0-9a-fA-F]/.freeze
27
- UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/.freeze
28
- NONASCII = /[#{S}]/.freeze
29
- ESCAPE = /#{UNICODE}|\\[ -~#{S}]/.freeze
30
- NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/.freeze
26
+ H = /[0-9a-fA-F]/
27
+ UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/
28
+ NONASCII = /[#{S}]/
29
+ ESCAPE = /#{UNICODE}|\\[ -~#{S}]/
30
+ NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/
31
31
 
32
32
  class Splitter
33
33
  def split(css)
@@ -93,7 +93,7 @@ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
93
93
  row.map do |header, cell|
94
94
  header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
95
95
  XPath.descendant(:td)[
96
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
96
+ XPath.string.n.is(cell) & header_xp.boolean & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
97
97
  ]
98
98
  end.reduce(:&)
99
99
  end
@@ -7,14 +7,14 @@ Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
7
7
  locator.reduce(xpath) do |xp, (header, cell)|
8
8
  header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
9
9
  cell_xp = XPath.descendant(:td)[
10
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
10
+ XPath.string.n.is(cell) & header_xp.boolean & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
11
11
  ]
12
12
  xp.where(cell_xp)
13
13
  end
14
14
  else
15
15
  initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
16
16
  tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }
17
- .reduce { |xp, cell| xp.where(cell) }
17
+ .reduce { |xp, cell| cell.where(xp) }
18
18
  xpath[initial_td[tds]]
19
19
  end
20
20
  end
@@ -21,12 +21,30 @@ require 'capybara/selector/definition'
21
21
  # * **:xpath** - Select elements by XPath expression
22
22
  # * Locator: An XPath expression
23
23
  #
24
+ # ```ruby
25
+ # page.html # => '<input>'
26
+ #
27
+ # page.find :xpath, './/input'
28
+ # ```
29
+ #
24
30
  # * **:css** - Select elements by CSS selector
25
31
  # * Locator: A CSS selector
26
32
  #
33
+ # ```ruby
34
+ # page.html # => '<input>'
35
+ #
36
+ # page.find :css, 'input'
37
+ # ```
38
+ #
27
39
  # * **:id** - Select element by id
28
40
  # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
29
41
  #
42
+ # ```ruby
43
+ # page.html # => '<input id="field">'
44
+ #
45
+ # page.find :id, 'content'
46
+ # ```
47
+ #
30
48
  # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
31
49
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or
32
50
  # associated label text
@@ -43,12 +61,30 @@ require 'capybara/selector/definition'
43
61
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
44
62
  # * :validation_message (String, Regexp) - Matches the elements current validationMessage
45
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
+ #
46
74
  # * **:fieldset** - Select fieldset elements
47
75
  # * Locator: Matches id, {Capybara.configure test_id}, or contents of wrapped legend
48
76
  # * Filters:
49
77
  # * :legend (String) - Matches contents of wrapped legend
50
78
  # * :disabled (Boolean) - Match disabled fieldset?
51
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
+ #
52
88
  # * **:link** - Find links (`<a>` elements with an href attribute)
53
89
  # * Locator: Matches the id, {Capybara.configure test_id}, or title attributes, or the string content of the link,
54
90
  # or the alt attribute of a contained img element. By default this selector requires a link to have an href attribute.
@@ -57,6 +93,17 @@ require 'capybara/selector/definition'
57
93
  # * :alt (String) - Matches the alt attribute of a contained img element
58
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
59
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
+ #
60
107
  # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
61
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
62
109
  # * Filters:
@@ -66,11 +113,31 @@ require 'capybara/selector/definition'
66
113
  # * :type (String) - Matches the type attribute
67
114
  # * :disabled (Boolean, :all) - Match disabled buttons (Default: false)
68
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
+ #
69
126
  # * **:link_or_button** - Find links or buttons
70
127
  # * Locator: See :link and :button selectors
71
128
  # * Filters:
72
129
  # * :disabled (Boolean, :all) - Match disabled buttons? (Default: false)
73
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
+ #
74
141
  # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
75
142
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
76
143
  # * Filters:
@@ -83,6 +150,16 @@ require 'capybara/selector/definition'
83
150
  # * :valid (Boolean) - Match fields that are valid/invalid according to HTML5 form validation
84
151
  # * :validation_message (String, Regexp) - Matches the elements current validationMessage
85
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
+ #
86
163
  # * **:radio_button** - Find radio buttons
87
164
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
88
165
  # * Filters:
@@ -93,6 +170,18 @@ require 'capybara/selector/definition'
93
170
  # * :option (String, Regexp) - Match the current value
94
171
  # * :with - Alias of :option
95
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
+ #
96
185
  # * **:checkbox** - Find checkboxes
97
186
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
98
187
  # * Filters:
@@ -103,6 +192,15 @@ require 'capybara/selector/definition'
103
192
  # * :with (String, Regexp) - Match the current value
104
193
  # * :option - Alias of :with
105
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
+ #
106
204
  # * **:select** - Find select elements
107
205
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, placeholder, or associated label text
108
206
  # * Filters:
@@ -117,12 +215,40 @@ require 'capybara/selector/definition'
117
215
  # * :selected (String, Array<String>) - Match the selection(s)
118
216
  # * :with_selected (String, Array<String>) - Partial match the selection(s)
119
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
+ #
120
235
  # * **:option** - Find option elements
121
236
  # * Locator: Match text of option
122
237
  # * Filters:
123
238
  # * :disabled (Boolean) - Match disabled option
124
239
  # * :selected (Boolean) - Match selected option
125
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
+ #
126
252
  # * **:datalist_input** - Find input field with datalist completion
127
253
  # * Locator: Matches against the id, {Capybara.configure test_id} attribute, name,
128
254
  # placeholder, or associated label text
@@ -133,11 +259,42 @@ require 'capybara/selector/definition'
133
259
  # * :options (Array<String>) - Exact match options
134
260
  # * :with_options (Array<String>) - Partial match options
135
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
+ #
136
279
  # * **:datalist_option** - Find datalist option
137
280
  # * Locator: Match text or value of option
138
281
  # * Filters:
139
282
  # * :disabled (Boolean) - Match disabled option
140
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
+ #
141
298
  # * **:file_field** - Find file input elements
142
299
  # * Locator: Match id, {Capybara.configure test_id} attribute, name, or associated label text
143
300
  # * Filters:
@@ -145,11 +302,31 @@ require 'capybara/selector/definition'
145
302
  # * :disabled (Boolean, :all) - Match disabled field? (Default: false)
146
303
  # * :multiple (Boolean) - Match field that accepts multiple values
147
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
+ #
148
316
  # * **:label** - Find label elements
149
317
  # * Locator: Match id, {Capybara.configure test_id}, or text contents
150
318
  # * Filters:
151
319
  # * :for (Element, String, Regexp) - The element or id of the element associated with the label
152
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
+ #
153
330
  # * **:table** - Find table elements
154
331
  # * Locator: id, {Capybara.configure test_id}, or caption text of table
155
332
  # * Filters:
@@ -159,19 +336,93 @@ require 'capybara/selector/definition'
159
336
  # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match `<td>` data - visibility of `<td>` elements is not considered
160
337
  # * :cols (Array<Array<String>>) - Match all `<td>`s - visibility of `<td>` elements is not considered
161
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
+ #
162
376
  # * **:table_row** - Find table row
163
377
  # * Locator: Array<String>, Hash<String, String> table row `<td>` contents - visibility of `<td>` elements is not considered
164
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
+ #
165
399
  # * **:frame** - Find frame/iframe elements
166
400
  # * Locator: Match id, {Capybara.configure test_id} attribute, or name
167
401
  # * Filters:
168
402
  # * :name (String) - Match name attribute
169
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
+ #
170
412
  # * **:element**
171
413
  # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
172
414
  # * Filters:
173
415
  # * :\<any> (String, Regexp) - Match on any specified element attribute
174
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
+ #
175
426
  class Capybara::Selector; end # rubocop:disable Lint/EmptyClass
176
427
 
177
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', '< 4.8.0')
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.142.7').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
@@ -157,8 +148,8 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
157
148
  native_active_element.send_keys(*args)
158
149
  end
159
150
 
160
- def save_screenshot(path, **_options)
161
- browser.save_screenshot(path)
151
+ def save_screenshot(path, **options)
152
+ browser.save_screenshot(path, **options)
162
153
  end
163
154
 
164
155
  def reset!
@@ -313,16 +304,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
313
304
  ::Selenium::WebDriver::Error::NoSuchElementError, # IE
314
305
  ::Selenium::WebDriver::Error::InvalidArgumentError # IE
315
306
  ].tap do |errors|
316
- unless selenium_4?
317
- ::Selenium::WebDriver.logger.suppress_deprecations do
318
- errors.push(
319
- ::Selenium::WebDriver::Error::UnhandledError,
320
- ::Selenium::WebDriver::Error::ElementNotVisibleError,
321
- ::Selenium::WebDriver::Error::InvalidElementStateError,
322
- ::Selenium::WebDriver::Error::ElementNotSelectableError
323
- )
324
- end
325
- end
326
307
  if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
327
308
  errors.push(::Selenium::WebDriver::Error::DetachedShadowRootError)
328
309
  end
@@ -335,10 +316,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
335
316
 
336
317
  private
337
318
 
338
- def selenium_4?
339
- defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)
340
- end
341
-
342
319
  def native_args(args)
343
320
  args.map { |arg| arg.is_a?(Capybara::Selenium::Node) ? arg.native : arg }
344
321
  end
@@ -361,10 +338,7 @@ private
361
338
  end
362
339
 
363
340
  def unhandled_alert_errors
364
- @unhandled_alert_errors ||= with_legacy_error(
365
- [Selenium::WebDriver::Error::UnexpectedAlertOpenError],
366
- 'UnhandledAlertError'
367
- )
341
+ @unhandled_alert_errors ||= [Selenium::WebDriver::Error::UnexpectedAlertOpenError]
368
342
  end
369
343
 
370
344
  def delete_all_cookies
@@ -454,17 +428,7 @@ private
454
428
  end
455
429
 
456
430
  def find_modal_errors
457
- @find_modal_errors ||= with_legacy_error([Selenium::WebDriver::Error::TimeoutError], 'TimeOutError')
458
- end
459
-
460
- def with_legacy_error(errors, legacy_error)
461
- errors.tap do |errs|
462
- unless selenium_4?
463
- ::Selenium::WebDriver.logger.suppress_deprecations do
464
- errs << Selenium::WebDriver::Error.const_get(legacy_error)
465
- end
466
- end
467
- end
431
+ @find_modal_errors ||= [Selenium::WebDriver::Error::TimeoutError]
468
432
  end
469
433
 
470
434
  def silenced_unknown_error_message?(msg)
@@ -476,16 +440,12 @@ private
476
440
  end
477
441
 
478
442
  def unwrap_script_result(arg)
479
- # TODO: move into the case when we drop support for Selenium < 4.1
480
- element_types = [Selenium::WebDriver::Element]
481
- element_types.push(Selenium::WebDriver::ShadowRoot) if defined?(Selenium::WebDriver::ShadowRoot)
482
-
483
443
  case arg
484
444
  when Array
485
445
  arg.map { |arr| unwrap_script_result(arr) }
486
446
  when Hash
487
447
  arg.transform_values! { |value| unwrap_script_result(value) }
488
- when *element_types
448
+ when Selenium::WebDriver::Element, Selenium::WebDriver::ShadowRoot
489
449
  build_node(arg)
490
450
  else
491
451
  arg
@@ -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) && (Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('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
@@ -132,13 +132,11 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
132
132
  target = click_options.coords? ? nil : native
133
133
  if click_options.delay.zero?
134
134
  action.context_click(target)
135
- elsif w3c?
135
+ else
136
136
  action.move_to(target) if target
137
137
  action.pointer_down(:right).then do |act|
138
138
  action_pause(act, click_options.delay)
139
139
  end.pointer_up(:right)
140
- else
141
- raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
142
140
  end
143
141
  end
144
142
  end
@@ -219,8 +217,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
219
217
  end
220
218
 
221
219
  def shadow_root
222
- raise 'You must be using Selenium 4.1+ for shadow_root support' unless native.respond_to? :shadow_root
223
-
224
220
  root = native.shadow_root
225
221
  root && build_node(native.shadow_root)
226
222
  end
@@ -416,14 +412,8 @@ private
416
412
  actions = browser_action.tap do |acts|
417
413
  if click_options.coords?
418
414
  if click_options.center_offset?
419
- if Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('4.3')
420
- acts.move_to(native, *click_options.coords)
421
- else
422
- ::Selenium::WebDriver.logger.suppress_deprecations do
423
- acts.move_to(native).move_by(*click_options.coords)
424
- end
425
- end
426
- elsif Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('4.3')
415
+ acts.move_to(native, *click_options.coords)
416
+ else
427
417
  right_by, down_by = *click_options.coords
428
418
  size = native.size
429
419
  left_offset = (size[:width] / 2).to_i
@@ -431,10 +421,6 @@ private
431
421
  left = -left_offset + right_by
432
422
  top = -top_offset + down_by
433
423
  acts.move_to(native, left, top)
434
- else
435
- ::Selenium::WebDriver.logger.suppress_deprecations do
436
- acts.move_to(native, *click_options.coords)
437
- end
438
424
  end
439
425
  else
440
426
  acts.move_to(native)
@@ -475,21 +461,8 @@ private
475
461
  browser.capabilities
476
462
  end
477
463
 
478
- def w3c?
479
- (defined?(Selenium::WebDriver::VERSION) && (Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('4'))) ||
480
- capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
481
- end
482
-
483
464
  def action_pause(action, duration)
484
- if w3c?
485
- if Gem::Version.new(Selenium::WebDriver::VERSION) >= Gem::Version.new('4.2')
486
- action.pause(device: action.pointer_inputs.first, duration: duration)
487
- else
488
- action.pause(action.pointer_inputs.first, duration)
489
- end
490
- else
491
- action.pause(duration)
492
- end
465
+ action.pause(device: action.pointer_inputs.first, duration: duration)
493
466
  end
494
467
 
495
468
  def normalize_keys(keys)