capybara 3.37.1 → 3.38.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +34 -4
  3. data/README.md +23 -11
  4. data/lib/capybara/helpers.rb +5 -1
  5. data/lib/capybara/node/base.rb +2 -1
  6. data/lib/capybara/queries/base_query.rb +2 -2
  7. data/lib/capybara/queries/selector_query.rb +4 -2
  8. data/lib/capybara/queries/text_query.rb +1 -1
  9. data/lib/capybara/rack_test/browser.rb +8 -2
  10. data/lib/capybara/rack_test/form.rb +29 -7
  11. data/lib/capybara/registrations/servers.rb +17 -9
  12. data/lib/capybara/selector/definition.rb +1 -1
  13. data/lib/capybara/selector/filter_set.rb +4 -5
  14. data/lib/capybara/selector/regexp_disassembler.rb +2 -5
  15. data/lib/capybara/selenium/driver.rb +3 -0
  16. data/lib/capybara/selenium/extensions/html5_drag.rb +2 -4
  17. data/lib/capybara/selenium/logger_suppressor.rb +4 -0
  18. data/lib/capybara/selenium/node.rb +52 -15
  19. data/lib/capybara/selenium/nodes/firefox_node.rb +2 -2
  20. data/lib/capybara/selenium/nodes/safari_node.rb +2 -2
  21. data/lib/capybara/server/animation_disabler.rb +20 -20
  22. data/lib/capybara/server/middleware.rb +1 -1
  23. data/lib/capybara/session/config.rb +3 -1
  24. data/lib/capybara/session.rb +11 -9
  25. data/lib/capybara/spec/session/attach_file_spec.rb +6 -0
  26. data/lib/capybara/spec/session/check_spec.rb +1 -0
  27. data/lib/capybara/spec/session/current_scope_spec.rb +1 -1
  28. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  29. data/lib/capybara/spec/session/find_spec.rb +1 -1
  30. data/lib/capybara/spec/session/has_ancestor_spec.rb +2 -2
  31. data/lib/capybara/spec/session/has_button_spec.rb +6 -0
  32. data/lib/capybara/spec/session/has_link_spec.rb +6 -0
  33. data/lib/capybara/spec/session/has_select_spec.rb +6 -0
  34. data/lib/capybara/spec/session/has_text_spec.rb +4 -8
  35. data/lib/capybara/spec/session/node_spec.rb +24 -1
  36. data/lib/capybara/spec/session/reset_session_spec.rb +13 -0
  37. data/lib/capybara/spec/session/within_spec.rb +13 -0
  38. data/lib/capybara/spec/spec_helper.rb +8 -2
  39. data/lib/capybara/spec/test_app.rb +25 -6
  40. data/lib/capybara/spec/views/form.erb +13 -0
  41. data/lib/capybara/spec/views/with_html.erb +2 -2
  42. data/lib/capybara/spec/views/with_scope.erb +2 -2
  43. data/lib/capybara/version.rb +1 -1
  44. data/lib/capybara.rb +4 -2
  45. data/spec/capybara_spec.rb +12 -0
  46. data/spec/counter_spec.rb +35 -0
  47. data/spec/dsl_spec.rb +2 -0
  48. data/spec/minitest_spec.rb +4 -0
  49. data/spec/minitest_spec_spec.rb +4 -0
  50. data/spec/per_session_config_spec.rb +1 -1
  51. data/spec/rack_test_spec.rb +8 -0
  52. data/spec/rspec/shared_spec_matchers.rb +1 -1
  53. data/spec/rspec_spec.rb +2 -2
  54. data/spec/selector_spec.rb +2 -2
  55. data/spec/selenium_spec_chrome.rb +2 -0
  56. data/spec/selenium_spec_chrome_remote.rb +4 -2
  57. data/spec/selenium_spec_edge.rb +2 -0
  58. data/spec/selenium_spec_firefox.rb +11 -5
  59. data/spec/selenium_spec_firefox_remote.rb +4 -2
  60. data/spec/selenium_spec_ie.rb +3 -1
  61. data/spec/selenium_spec_safari.rb +2 -0
  62. data/spec/shared_selenium_session.rb +3 -4
  63. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7283f19c4364fa2c19379e8da670105ebf59754937fc80117d975cd89959b87
4
- data.tar.gz: b4fe7210ee210667602e9ec0a9882a1a34d8a64ceddae4df888aba0dd6974940
3
+ metadata.gz: 163cb499d23bf17278c9912462fb3fb337b2d28b764a95c5ed3f9c4af3f1c317
4
+ data.tar.gz: 7010ef4e7f6af6c4d78ae58a05846c2607864d6fe24d05c49747fc1e1ded8675
5
5
  SHA512:
6
- metadata.gz: 71de08c12fdd479453fa743e0a7ef858e72ca96d9a32d2ecb0b1721b365a1f1e494309432b52536dcac5669af78fe50e60b1a9f886f0b08a2ac2fd378abfc52e
7
- data.tar.gz: 91a2e75a15768ef02e8c9b9f1400c4ee1dd2e1e2562b4733dc520afddc4c71be962fcd5c0379481fa65938d0776cfe5434c75f5faf0e512246abca48b06b0c00
6
+ metadata.gz: 66ac2676f22d83ec1a4478f68a8f7f095c9c9f9396eb1b5c1bf34803e15e19ed02c1f1342731d55787219ddd2d9d0829eb0c76ef702c4e07ac3a0b19a9c2aaec
7
+ data.tar.gz: f35c37f9ccdcdd58c1c76c80cf58eb8c62b0878bb40ff1dd6191978978409b41adabbc34013868e30a5ec4f33d077532ad8c1a90fdf0138dd524703c5168a001
data/History.md CHANGED
@@ -1,5 +1,35 @@
1
+ # Version 3.38.0
2
+ Release date: 2022-11-03
3
+
4
+ ### Changed
5
+
6
+ * Capybara.w3c_click_offset now defaults to true. If you need click offsets to be from the elements top left corner set it to false in your config
7
+
8
+ ### Added
9
+
10
+ * Support Selenium 4.3 changes to click offset calculations
11
+ * `click`, `double_click`, `right_click` can now be called on the session to click the currently scoped element (or document)
12
+ * `Session#within` now passes the scoped element to the block
13
+ * Support rack-test 2+
14
+ * Retry interval is now configurable [Masahiro NOMOTO]
15
+ * Support Puma 6 - Issue #2590
16
+ * Selenium: DetachedShadowRootError is treated as an invalid element error [Perryn Fowler]
17
+ * Selenium: When inspected shadow roots will have a tag name of "ShadowRoot"
18
+ * `evaluate_async_script` added to Session::DSL_METHODS [Henry Blyth]
19
+
20
+ ### Fixed
21
+
22
+ * Use higher precision clock in Capybara::Helpers::Timer if available
23
+ * rack-test driver behavior with \r\n - Issue #2547 [Stefan Hoffmann]
24
+ * Updated for deprecation of positional parameters in Selenium::WebDriver::ActionBuilder#pause
25
+ * Explicitly set cause on server raised errors
26
+ * Options no longer duplicated in have_xxx invalid option error message [Yudai Takada]
27
+ * Animation disabler is now threadsafe [Daniel Sheppard]
28
+ * Server connection count tracking [Oleksandr K.]
29
+ * Ensure scopes are reset when session is [Henry Blyth]
30
+
1
31
  # Version 3.37.1
2
- Relesae date: 2022-05-09
32
+ Release date: 2022-05-09
3
33
 
4
34
  ### Fixed
5
35
 
@@ -810,7 +840,7 @@ Release date: 2018-03-23
810
840
 
811
841
  ### Changed
812
842
 
813
- * Visibile text whitespace is no longer fully normalized in favor of being more in line with the WebDriver spec for visible text
843
+ * Visible text whitespace is no longer fully normalized in favor of being more in line with the WebDriver spec for visible text
814
844
  * Drivers are expected to close extra windows when resetting the session
815
845
  * Selenium driver supports Date/Time when filling in date/time/datetime-local inputs
816
846
  * `current_url` returns the url for the top level browsing context
@@ -1216,7 +1246,7 @@ Release date: 2016-01-27
1216
1246
 
1217
1247
  # Version 2.6.0
1218
1248
 
1219
- Relase date: 2016-01-17
1249
+ Release date: 2016-01-17
1220
1250
 
1221
1251
  ### Fixed
1222
1252
 
@@ -1280,7 +1310,7 @@ Release date: 2014-10-13
1280
1310
 
1281
1311
  # Version 2.4.3
1282
1312
 
1283
- Relase date: 2014-09-21
1313
+ Release date: 2014-09-21
1284
1314
 
1285
1315
  ### Fixed
1286
1316
 
data/README.md CHANGED
@@ -161,7 +161,7 @@ You can now write your specs like so:
161
161
  ```ruby
162
162
  describe "the signin process", type: :feature do
163
163
  before :each do
164
- User.make(email: 'user@example.com', password: 'password')
164
+ User.create(email: 'user@example.com', password: 'password')
165
165
  end
166
166
 
167
167
  it "signs me in" do
@@ -192,7 +192,7 @@ Capybara also comes with a built in DSL for creating descriptive acceptance test
192
192
  ```ruby
193
193
  feature "Signing in" do
194
194
  background do
195
- User.make(email: 'user@example.com', password: 'caplin')
195
+ User.create(email: 'user@example.com', password: 'caplin')
196
196
  end
197
197
 
198
198
  scenario "Signing in with correct credentials" do
@@ -205,7 +205,7 @@ feature "Signing in" do
205
205
  expect(page).to have_content 'Success'
206
206
  end
207
207
 
208
- given(:other_user) { User.make(email: 'other@example.com', password: 'rous') }
208
+ given(:other_user) { User.create(email: 'other@example.com', password: 'rous') }
209
209
 
210
210
  scenario "Signing in as another user" do
211
211
  visit '/sessions/new'
@@ -336,7 +336,7 @@ By default, Capybara uses the `:rack_test` driver, which is fast but limited: it
336
336
  does not support JavaScript, nor is it able to access HTTP resources outside of
337
337
  your Rack application, such as remote APIs and OAuth services. To get around
338
338
  these limitations, you can set up a different default driver for your features.
339
- For example if you'd prefer to run everything in Selenium, you could do:
339
+ For example, if you'd prefer to run everything in Selenium, you could do:
340
340
 
341
341
  ```ruby
342
342
  Capybara.default_driver = :selenium # :selenium_chrome and :selenium_chrome_headless are also registered
@@ -630,7 +630,7 @@ JS
630
630
 
631
631
  ### <a name="modals"></a>Modals
632
632
 
633
- In drivers which support it, you can accept, dismiss and respond to alerts, confirms and prompts.
633
+ In drivers which support it, you can accept, dismiss and respond to alerts, confirms, and prompts.
634
634
 
635
635
  You can accept or dismiss alert messages by wrapping the code that produces an alert in a block:
636
636
 
@@ -781,7 +781,7 @@ expect(page).to have_content('baz')
781
781
  If clicking on the *foo* link triggers an asynchronous process, such as
782
782
  an Ajax request, which, when complete will add the *bar* link to the page,
783
783
  clicking on the *bar* link would be expected to fail, since that link doesn't
784
- exist yet. However Capybara is smart enough to retry finding the link for a
784
+ exist yet. However, Capybara is smart enough to retry finding the link for a
785
785
  brief period of time before giving up and throwing an error. The same is true of
786
786
  the next line, which looks for the content *baz* on the page; it will retry
787
787
  looking for that content for a brief time. You can adjust how long this period
@@ -795,13 +795,25 @@ Be aware that because of this behaviour, the following two statements are **not*
795
795
  equivalent, and you should **always** use the latter!
796
796
 
797
797
  ```ruby
798
- !page.has_xpath?('a')
799
- page.has_no_xpath?('a')
798
+ # Given use of a driver where the page is loaded when visit returns
799
+ # and that Capybara.predicates_wait is `true`
800
+ # consider a page where the `a` tag is removed through AJAX after 1s
801
+ visit(some_path)
802
+ !page.has_xpath?('a') # is false
803
+ page.has_no_xpath?('a') # is true
800
804
  ```
801
805
 
802
- The former would immediately fail because the content has not yet been removed.
803
- Only the latter would wait for the asynchronous process to remove the content
804
- from the page.
806
+ First expression:
807
+ - `has_xpath?('a')` is called right after `visit` returns. It is `true` because the link has not yet been removed
808
+ - Capybara does not wait upon successful predicates/assertions, therefore **has_xpath? returns `true` immediately**
809
+ - The expression returns `false` (because it is negated with the leading `!`)
810
+
811
+ Second expression:
812
+ - `has_no_xpath?('a')` is called right after `visit` returns. It is `false` because the link has not yet been removed.
813
+ - Capybara waits upon failed predicates/assertions, therefore **has_no_xpath? does not return `false` immediately**
814
+ - Capybara will periodically re-check the predicate/assertion up to the `default_max_wait_time` defined
815
+ - after 1s, the predicate becomes `true` (because the link has been removed)
816
+ - The expression returns `true`
805
817
 
806
818
  Capybara's RSpec matchers, however, are smart enough to handle either form.
807
819
  The two following statements are functionally equivalent:
@@ -85,7 +85,11 @@ module Capybara
85
85
  Kernel.warn(message, uplevel: uplevel)
86
86
  end
87
87
 
88
- if defined?(Process::CLOCK_MONOTONIC)
88
+ if defined?(Process::CLOCK_MONOTONIC_RAW)
89
+ def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_RAW; end
90
+ elsif defined?(Process::CLOCK_MONOTONIC_PRECISE)
91
+ def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC_PRECISE; end
92
+ elsif defined?(Process::CLOCK_MONOTONIC)
89
93
  def monotonic_time; Process.clock_gettime Process::CLOCK_MONOTONIC; end
90
94
  else
91
95
  def monotonic_time; Time.now.to_f; end
@@ -77,6 +77,7 @@ module Capybara
77
77
  return yield if session.synchronized
78
78
 
79
79
  seconds = session_options.default_max_wait_time if [nil, true].include? seconds
80
+ interval = session_options.default_retry_interval
80
81
  session.synchronized = true
81
82
  timer = Capybara::Helpers.timer(expire_in: seconds)
82
83
  begin
@@ -88,7 +89,7 @@ module Capybara
88
89
  if driver.wait?
89
90
  raise e if timer.expired?
90
91
 
91
- sleep(0.01)
92
+ sleep interval
92
93
  reload if session_options.automatic_reload
93
94
  else
94
95
  old_base = @base
@@ -79,8 +79,8 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.begin ? between.first : 1} and" \
83
- " #{between.end ? between.last : 'infinite'} times"
82
+ message << " between #{between.begin ? between.first : 1} and " \
83
+ "#{between.end ? between.last : 'infinite'} times"
84
84
  elsif maximum
85
85
  message << " at most #{occurrences maximum}"
86
86
  elsif minimum
@@ -272,7 +272,7 @@ module Capybara
272
272
  end
273
273
 
274
274
  def valid_keys
275
- VALID_KEYS + custom_keys
275
+ (VALID_KEYS + custom_keys).uniq
276
276
  end
277
277
 
278
278
  def matches_node_filters?(node, errors)
@@ -570,7 +570,9 @@ module Capybara
570
570
  when :visible
571
571
  node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
572
572
  when :hidden
573
- (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
573
+ # TODO: check why the 'visbile' cache spelling mistake wasn't caught in a test
574
+ # (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
575
+ (node.initial_cache[:visible] == false) || (node.initial_cache[:visible].nil? && !node.visible?)
574
576
  else
575
577
  true
576
578
  end
@@ -13,7 +13,7 @@ module Capybara
13
13
  self.session_options = session_options
14
14
 
15
15
  if expected_text.nil? && !exact?
16
- warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. '\
16
+ warn 'Checking for expected text of nil is confusing and/or pointless since it will always match. ' \
17
17
  "Please specify a string or regexp instead. #{Capybara::Helpers.filter_backtrace(caller)}"
18
18
  end
19
19
 
@@ -31,11 +31,17 @@ class Capybara::RackTest::Browser
31
31
  request(last_request.fullpath, last_request.env)
32
32
  end
33
33
 
34
- def submit(method, path, attributes)
34
+ def submit(method, path, attributes, content_type: nil)
35
35
  path = request_path if path.nil? || path.empty?
36
36
  uri = build_uri(path)
37
37
  uri.query = '' if method.to_s.casecmp('get').zero?
38
- process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => referer_url)
38
+ process_and_follow_redirects(
39
+ method,
40
+ uri.to_s,
41
+ attributes,
42
+ 'HTTP_REFERER' => referer_url,
43
+ 'CONTENT_TYPE' => content_type
44
+ )
39
45
  end
40
46
 
41
47
  def follow(method, path, **attributes)
@@ -16,6 +16,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
16
16
  def path; @empty_file.path; end
17
17
  def size; 0; end
18
18
  def read; ''; end
19
+ def append_to(_); end
20
+ def set_encoding(_); end # rubocop:disable Naming/AccessorMethodName
19
21
  end
20
22
 
21
23
  def params(button)
@@ -28,19 +30,31 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
28
30
 
29
31
  form_elements = native.xpath(form_elements_xpath).reject { |el| submitter?(el) && (el != button.native) }
30
32
 
31
- form_elements.each_with_object(make_params) do |field, params|
33
+ form_params = form_elements.each_with_object({}.compare_by_identity) do |field, params|
32
34
  case field.name
33
35
  when 'input', 'button' then add_input_param(field, params)
34
36
  when 'select' then add_select_param(field, params)
35
37
  when 'textarea' then add_textarea_param(field, params)
36
38
  end
39
+ end
40
+
41
+ form_params.each_with_object(make_params) do |(name, value), params|
42
+ merge_param!(params, name, value)
37
43
  end.to_params_hash
44
+
45
+ # form_elements.each_with_object(make_params) do |field, params|
46
+ # case field.name
47
+ # when 'input', 'button' then add_input_param(field, params)
48
+ # when 'select' then add_select_param(field, params)
49
+ # when 'textarea' then add_textarea_param(field, params)
50
+ # end
51
+ # end.to_params_hash
38
52
  end
39
53
 
40
54
  def submit(button)
41
55
  action = button&.[]('formaction') || native['action']
42
56
  method = button&.[]('formmethod') || request_method
43
- driver.submit(method, action.to_s, params(button))
57
+ driver.submit(method, action.to_s, params(button), content_type: native['enctype'])
44
58
  end
45
59
 
46
60
  def multipart?
@@ -86,6 +100,8 @@ private
86
100
 
87
101
  Capybara::RackTest::Node.new(driver, field).value.to_s
88
102
  when 'file'
103
+ return if value.empty? && params.keys.include?(name) && Rack::Test::VERSION.to_f >= 2.0 # rubocop:disable Performance/InefficientHashSearch
104
+
89
105
  if multipart?
90
106
  file_to_upload(value)
91
107
  else
@@ -94,7 +110,8 @@ private
94
110
  else
95
111
  value
96
112
  end
97
- merge_param!(params, name, value)
113
+ # merge_param!(params, name, value)
114
+ params[name] = value
98
115
  end
99
116
 
100
117
  def file_to_upload(filename)
@@ -107,18 +124,23 @@ private
107
124
  end
108
125
 
109
126
  def add_select_param(field, params)
127
+ name = field['name']
110
128
  if field.has_attribute?('multiple')
111
- field.xpath('.//option[@selected]').each do |option|
112
- merge_param!(params, field['name'], (option['value'] || option.text).to_s)
129
+ value = field.xpath('.//option[@selected]').map do |option|
130
+ # merge_param!(params, field['name'], (option['value'] || option.text).to_s)
131
+ (option['value'] || option.text).to_s
113
132
  end
133
+ params[name] = value unless value.empty?
114
134
  else
115
135
  option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
116
- merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
136
+ # merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
137
+ params[name] = (option['value'] || option.text).to_s if option
117
138
  end
118
139
  end
119
140
 
120
141
  def add_textarea_param(field, params)
121
- merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
142
+ # merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n"))
143
+ params[field['name']] = field['_capybara_raw_value'].to_s.gsub(/\r?\n/, "\r\n")
122
144
  end
123
145
 
124
146
  def submitter?(el)
@@ -10,7 +10,7 @@ Capybara.register_server :webrick do |app, port, host, **options|
10
10
  Rack::Handler::WEBrick.run(app, **options)
11
11
  end
12
12
 
13
- Capybara.register_server :puma do |app, port, host, **options|
13
+ Capybara.register_server :puma do |app, port, host, **options| # rubocop:disable Metrics/BlockLength
14
14
  begin
15
15
  require 'rack/handler/puma'
16
16
  rescue LoadError
@@ -29,17 +29,25 @@ Capybara.register_server :puma do |app, port, host, **options|
29
29
 
30
30
  conf = Rack::Handler::Puma.config(app, options)
31
31
  conf.clamp
32
- events = conf.options[:Silent] ? ::Puma::Events.strings : ::Puma::Events.stdio
33
32
 
34
33
  puma_ver = Gem::Version.new(Puma::Const::PUMA_VERSION)
35
34
  require_relative 'patches/puma_ssl' if Gem::Requirement.new('>=4.0.0', '< 4.1.0').satisfied_by?(puma_ver)
36
35
 
37
- events.log 'Capybara starting Puma...'
38
- events.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
39
- events.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
40
-
41
- Puma::Server.new(conf.app, events, conf.options).tap do |s|
42
- s.binder.parse conf.options[:binds], s.events
43
- s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads]
36
+ logger = (defined?(::Puma::LogWriter) ? ::Puma::LogWriter : ::Puma::Events).then do |cls|
37
+ conf.options[:Silent] ? cls.strings : cls.stdio
38
+ end
39
+ conf.options[:log_writer] = logger
40
+
41
+ logger.log 'Capybara starting Puma...'
42
+ logger.log "* Version #{Puma::Const::PUMA_VERSION} , codename: #{Puma::Const::CODE_NAME}"
43
+ logger.log "* Min threads: #{conf.options[:min_threads]}, max threads: #{conf.options[:max_threads]}"
44
+
45
+ Puma::Server.new(
46
+ conf.app,
47
+ defined?(::Puma::LogWriter) ? nil : logger,
48
+ conf.options
49
+ ).tap do |s|
50
+ s.binder.parse conf.options[:binds], (s.log_writer rescue s.events) # rubocop:disable Style/RescueModifier
51
+ s.min_threads, s.max_threads = conf.options[:min_threads], conf.options[:max_threads] if s.respond_to? :min_threads=
44
52
  end.run.join
45
53
  end
@@ -203,7 +203,7 @@ module Capybara
203
203
 
204
204
  ##
205
205
  #
206
- # Set the default visibility mode that shouble be used if no visibile option is passed when using the selector.
206
+ # Set the default visibility mode that should be used if no visible option is passed when using the selector.
207
207
  # If not specified will default to the behavior indicated by Capybara.ignore_hidden_elements
208
208
  #
209
209
  # @param [Symbol] default_visibility Only find elements with the specified visibility:
@@ -101,11 +101,10 @@ module Capybara
101
101
  private
102
102
 
103
103
  def options_with_defaults(options)
104
- expression_filters.chain(node_filters)
105
- .select { |_n, filter| filter.default? }
106
- .each_with_object(options.dup) do |(name, filter), opts|
107
- opts[name] = filter.default unless opts.key?(name)
108
- end
104
+ expression_filters
105
+ .chain(node_filters)
106
+ .filter_map { |name, filter| [name, filter.default] if filter.default? }
107
+ .to_h.merge!(options)
109
108
  end
110
109
 
111
110
  def add_filter(name, filter_class, *types, matcher: nil, **options, &block)
@@ -69,11 +69,8 @@ module Capybara
69
69
  suffixes = [[]]
70
70
  strs.reverse_each do |str|
71
71
  if str.is_a? Set
72
- prefixes = str.each_with_object([]) { |s, memo| memo.concat combine(s) }
73
-
74
- result = []
75
- prefixes.product(suffixes) { |pair| result << pair.flatten(1) }
76
- suffixes = result
72
+ prefixes = str.flat_map { |s| combine(s) }
73
+ suffixes = prefixes.product(suffixes).map { |pair| pair.flatten(1) }
77
74
  else
78
75
  suffixes.each { |arr| arr.unshift str }
79
76
  end
@@ -323,6 +323,9 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
323
323
  ]
324
324
  end
325
325
  end
326
+ if defined?(::Selenium::WebDriver::Error::DetachedShadowRootError)
327
+ errors.concat([::Selenium::WebDriver::Error::DetachedShadowRootError])
328
+ end
326
329
  end
327
330
  end
328
331
 
@@ -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
@@ -18,6 +18,10 @@ module Capybara
18
18
  end
19
19
  end
20
20
 
21
+ def warn(*args, **opts)
22
+ super unless @suppress_for_capybara
23
+ end
24
+
21
25
  def suppress_deprecations
22
26
  prev_suppress_for_capybara, @suppress_for_capybara = @suppress_for_capybara, true
23
27
  yield
@@ -10,6 +10,8 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
10
10
  include Capybara::Selenium::Scroll
11
11
 
12
12
  def visible_text
13
+ raise NotImplementedError, 'Getting visible text is not currently supported directly on shadow roots' if shadow_root?
14
+
13
15
  native.text
14
16
  end
15
17
 
@@ -37,9 +39,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
37
39
  end
38
40
 
39
41
  def style(styles)
40
- styles.each_with_object({}) do |style, result|
41
- result[style] = native.css_value(style)
42
- end
42
+ styles.to_h { |style| [style, native.css_value(style)] }
43
43
  end
44
44
 
45
45
  ##
@@ -115,11 +115,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
115
115
  action.click(target)
116
116
  else
117
117
  action.click_and_hold(target)
118
- if w3c?
119
- action.pause(action.pointer_inputs.first, click_options.delay)
120
- else
121
- action.pause(click_options.delay)
122
- end
118
+ action_pause(action, click_options.delay)
123
119
  action.release
124
120
  end
125
121
  end
@@ -140,9 +136,9 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
140
136
  action.context_click(target)
141
137
  elsif w3c?
142
138
  action.move_to(target) if target
143
- action.pointer_down(:right)
144
- .pause(action.pointer_inputs.first, click_options.delay)
145
- .pointer_up(:right)
139
+ action.pointer_down(:right).then do |act|
140
+ action_pause(act, click_options.delay)
141
+ end.pointer_up(:right)
146
142
  else
147
143
  raise ArgumentError, 'Delay is not supported when right clicking with legacy (non-w3c) selenium driver'
148
144
  end
@@ -184,7 +180,12 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
184
180
  end
185
181
 
186
182
  def tag_name
187
- @tag_name ||= native.tag_name.downcase
183
+ @tag_name ||=
184
+ if native.respond_to? :tag_name
185
+ native.tag_name.downcase
186
+ else
187
+ shadow_root? ? 'ShadowRoot' : 'Unknown'
188
+ end
188
189
  end
189
190
 
190
191
  def visible?; boolean_attr(native.displayed?); end
@@ -415,10 +416,30 @@ private
415
416
 
416
417
  def action_with_modifiers(click_options)
417
418
  actions = browser_action.tap do |acts|
418
- if click_options.center_offset? && click_options.coords?
419
- acts.move_to(native).move_by(*click_options.coords)
419
+ if click_options.coords?
420
+ if click_options.center_offset?
421
+ if Selenium::WebDriver::VERSION.to_f >= 4.3
422
+ acts.move_to(native, *click_options.coords)
423
+ else
424
+ ::Selenium::WebDriver.logger.suppress_deprecations do
425
+ acts.move_to(native).move_by(*click_options.coords)
426
+ end
427
+ end
428
+ elsif Selenium::WebDriver::VERSION.to_f >= 4.3
429
+ right_by, down_by = *click_options.coords
430
+ size = native.size
431
+ left_offset = (size[:width] / 2).to_i
432
+ top_offset = (size[:height] / 2).to_i
433
+ left = -left_offset + right_by
434
+ top = -top_offset + down_by
435
+ acts.move_to(native, left, top)
436
+ else
437
+ ::Selenium::WebDriver.logger.suppress_deprecations do
438
+ acts.move_to(native, *click_options.coords)
439
+ end
440
+ end
420
441
  else
421
- acts.move_to(native, *click_options.coords)
442
+ acts.move_to(native)
422
443
  end
423
444
  end
424
445
  modifiers_down(actions, click_options.keys)
@@ -461,6 +482,18 @@ private
461
482
  capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
462
483
  end
463
484
 
485
+ def action_pause(action, duration)
486
+ if w3c?
487
+ if Selenium::WebDriver::VERSION.to_f >= 4.2
488
+ action.pause(device: action.pointer_inputs.first, duration: duration)
489
+ else
490
+ action.pause(action.pointer_inputs.first, duration)
491
+ end
492
+ else
493
+ action.pause(duration)
494
+ end
495
+ end
496
+
464
497
  def normalize_keys(keys)
465
498
  keys.map do |key|
466
499
  case key
@@ -502,6 +535,10 @@ private
502
535
  id || type_or_id
503
536
  end
504
537
 
538
+ def shadow_root?
539
+ defined?(::Selenium::WebDriver::ShadowRoot) && native.is_a?(::Selenium::WebDriver::ShadowRoot)
540
+ end
541
+
505
542
  GET_XPATH_SCRIPT = <<~'JS'
506
543
  (function(el, xml){
507
544
  var xpath = '';
@@ -11,8 +11,8 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
11
11
  super
12
12
  rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
13
13
  if tag_name == 'tr'
14
- warn 'You are attempting to click a table row which has issues in geckodriver/marionette - '\
15
- 'see https://github.com/mozilla/geckodriver/issues/1228. Your test should probably be '\
14
+ warn 'You are attempting to click a table row which has issues in geckodriver/marionette - ' \
15
+ 'see https://github.com/mozilla/geckodriver/issues/1228 - Your test should probably be ' \
16
16
  'clicking on a table cell like a user would. Clicking the first cell in the row instead.'
17
17
  return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end
@@ -11,8 +11,8 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
11
11
  super
12
12
  rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
13
13
  if tag_name == 'tr'
14
- warn 'You are attempting to click a table row which has issues in safaridriver - '\
15
- 'Your test should probably be clicking on a table cell like a user would. '\
14
+ warn 'You are attempting to click a table row which has issues in safaridriver - ' \
15
+ 'Your test should probably be clicking on a table cell like a user would. ' \
16
16
  'Clicking the first cell in the row instead.'
17
17
  return find_css('th:first-child,td:first-child')[0].click(keys, **options)
18
18
  end