capybara 3.39.2 → 3.40.0

Sign up to get free protection for your applications and to get access to all the features.
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)