selenium-webdriver 4.35.0 → 4.47.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +111 -0
  3. data/Gemfile +1 -4
  4. data/LICENSE +1 -1
  5. data/NOTICE +1 -1
  6. data/README.md +43 -23
  7. data/bin/linux/selenium-manager +0 -0
  8. data/bin/macos/selenium-manager +0 -0
  9. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5971 -0
  10. data/bin/selenium-manager.cdx.json +11629 -0
  11. data/bin/windows/selenium-manager.exe +0 -0
  12. data/lib/selenium/server.rb +30 -5
  13. data/lib/selenium/webdriver/atoms/findElements.js +272 -54
  14. data/lib/selenium/webdriver/atoms/getAttribute.js +152 -6
  15. data/lib/selenium/webdriver/atoms/isDisplayed.js +385 -28
  16. data/lib/selenium/webdriver/bidi/browser.rb +71 -0
  17. data/lib/selenium/webdriver/bidi/browsing_context.rb +3 -2
  18. data/lib/selenium/webdriver/bidi/{log/filter_by.rb → error.rb} +20 -16
  19. data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
  20. data/lib/selenium/webdriver/bidi/network/cookies.rb +13 -9
  21. data/lib/selenium/webdriver/bidi/network/credentials.rb +4 -0
  22. data/lib/selenium/webdriver/bidi/network/headers.rb +4 -0
  23. data/lib/selenium/webdriver/bidi/network/intercepted_auth.rb +4 -0
  24. data/lib/selenium/webdriver/bidi/network/intercepted_item.rb +4 -0
  25. data/lib/selenium/webdriver/bidi/network/intercepted_request.rb +20 -4
  26. data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +24 -6
  27. data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
  28. data/lib/selenium/webdriver/bidi/network.rb +18 -9
  29. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +495 -0
  30. data/lib/selenium/webdriver/bidi/protocol/browser.rb +250 -0
  31. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +798 -0
  32. data/lib/selenium/webdriver/bidi/{log/base_log_entry.rb → protocol/domain.rb} +22 -11
  33. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +448 -0
  34. data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
  35. data/lib/selenium/webdriver/bidi/protocol/input.rb +354 -0
  36. data/lib/selenium/webdriver/bidi/protocol/log.rb +115 -0
  37. data/lib/selenium/webdriver/bidi/protocol/network.rb +720 -0
  38. data/lib/selenium/webdriver/bidi/protocol/permissions.rb +75 -0
  39. data/lib/selenium/webdriver/bidi/protocol/script.rb +1052 -0
  40. data/lib/selenium/webdriver/bidi/protocol/session.rb +286 -0
  41. data/lib/selenium/webdriver/bidi/protocol/speculation.rb +58 -0
  42. data/lib/selenium/webdriver/bidi/protocol/storage.rb +183 -0
  43. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +88 -0
  44. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +132 -0
  45. data/lib/selenium/webdriver/bidi/protocol.rb +42 -0
  46. data/lib/selenium/webdriver/bidi/serialization/record.rb +402 -0
  47. data/lib/selenium/webdriver/bidi/serialization/union.rb +156 -0
  48. data/lib/selenium/webdriver/bidi/serialization.rb +90 -0
  49. data/lib/selenium/webdriver/bidi/session.rb +4 -0
  50. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +1420 -0
  51. data/lib/selenium/webdriver/bidi/support/check_generated.rb +63 -0
  52. data/lib/selenium/webdriver/bidi/transport.rb +57 -0
  53. data/lib/selenium/webdriver/bidi.rb +4 -1
  54. data/lib/selenium/webdriver/chrome/driver.rb +4 -3
  55. data/lib/selenium/webdriver/chrome/service.rb +13 -0
  56. data/lib/selenium/webdriver/chromium/driver.rb +0 -1
  57. data/lib/selenium/webdriver/chromium/options.rb +23 -1
  58. data/lib/selenium/webdriver/chromium/profile.rb +6 -0
  59. data/lib/selenium/webdriver/common/child_process.rb +2 -1
  60. data/lib/selenium/webdriver/common/client_config.rb +97 -0
  61. data/lib/selenium/webdriver/common/driver.rb +13 -7
  62. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
  63. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
  64. data/lib/selenium/webdriver/common/driver_extensions/{has_bidi.rb → has_session_events.rb} +19 -7
  65. data/lib/selenium/webdriver/common/driver_finder.rb +35 -25
  66. data/lib/selenium/webdriver/common/error.rb +10 -3
  67. data/lib/selenium/webdriver/common/local_driver.rb +29 -12
  68. data/lib/selenium/webdriver/common/logger.rb +28 -0
  69. data/lib/selenium/webdriver/common/manager.rb +2 -0
  70. data/lib/selenium/webdriver/common/options.rb +41 -2
  71. data/lib/selenium/webdriver/common/platform.rb +1 -3
  72. data/lib/selenium/webdriver/common/proxy.rb +1 -9
  73. data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
  74. data/lib/selenium/webdriver/common/service.rb +4 -8
  75. data/lib/selenium/webdriver/common/service_manager.rb +36 -4
  76. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  77. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  78. data/lib/selenium/webdriver/common/virtual_authenticator/credential.rb +1 -1
  79. data/lib/selenium/webdriver/common/wait.rb +4 -1
  80. data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
  81. data/lib/selenium/webdriver/common.rb +2 -1
  82. data/lib/selenium/webdriver/devtools/response.rb +3 -3
  83. data/lib/selenium/webdriver/devtools.rb +1 -1
  84. data/lib/selenium/webdriver/edge/driver.rb +4 -3
  85. data/lib/selenium/webdriver/edge/service.rb +14 -0
  86. data/lib/selenium/webdriver/firefox/driver.rb +4 -6
  87. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  88. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  89. data/lib/selenium/webdriver/firefox/service.rb +31 -2
  90. data/lib/selenium/webdriver/ie/driver.rb +4 -3
  91. data/lib/selenium/webdriver/ie/service.rb +10 -0
  92. data/lib/selenium/webdriver/remote/bidi_bridge.rb +44 -10
  93. data/lib/selenium/webdriver/remote/bridge.rb +20 -14
  94. data/lib/selenium/webdriver/remote/driver.rb +15 -4
  95. data/lib/selenium/webdriver/remote/features.rb +26 -1
  96. data/lib/selenium/webdriver/remote/http/common.rb +57 -12
  97. data/lib/selenium/webdriver/remote/http/curb.rb +6 -8
  98. data/lib/selenium/webdriver/remote/http/default.rb +56 -39
  99. data/lib/selenium/webdriver/safari/driver.rb +4 -3
  100. data/lib/selenium/webdriver/safari/options.rb +23 -4
  101. data/lib/selenium/webdriver/safari.rb +1 -6
  102. data/lib/selenium/webdriver/support/block_event_listener.rb +5 -1
  103. data/lib/selenium/webdriver/support/color.rb +14 -14
  104. data/lib/selenium/webdriver/support/event_firing_bridge.rb +5 -1
  105. data/lib/selenium/webdriver/support/guards/guard.rb +30 -14
  106. data/lib/selenium/webdriver/support/guards.rb +11 -4
  107. data/lib/selenium/webdriver/version.rb +1 -1
  108. data/lib/selenium/webdriver.rb +7 -2
  109. data/selenium-webdriver.gemspec +1 -1
  110. metadata +33 -11
  111. data/lib/selenium/webdriver/bidi/log/console_log_entry.rb +0 -35
  112. data/lib/selenium/webdriver/bidi/log/generic_log_entry.rb +0 -33
  113. data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +0 -33
  114. data/lib/selenium/webdriver/bidi/log_inspector.rb +0 -147
@@ -40,7 +40,6 @@ module Selenium
40
40
  LOCK_FILES = %w[.parentlock parent.lock lock].freeze
41
41
 
42
42
  attr_reader :name, :log_file
43
- attr_writer :secure_ssl, :load_no_focus_lib
44
43
 
45
44
  class << self
46
45
  def ini
@@ -110,19 +109,28 @@ module Selenium
110
109
  end
111
110
 
112
111
  def port=(port)
112
+ WebDriver.logger.deprecate('Firefox::Profile#port=', 'the Service class', id: :firefox_profile)
113
113
  self[WEBDRIVER_PREFS[:port]] = port
114
114
  end
115
115
 
116
+ def secure_ssl=(value)
117
+ WebDriver.logger.deprecate('Firefox::Profile#secure_ssl=', id: :firefox_profile)
118
+ @secure_ssl = value
119
+ end
120
+
121
+ def load_no_focus_lib=(value)
122
+ WebDriver.logger.deprecate('Firefox::Profile#load_no_focus_lib=', id: :firefox_profile)
123
+ @load_no_focus_lib = value
124
+ end
125
+
116
126
  def log_file=(file)
117
127
  @log_file = file
118
128
  self[WEBDRIVER_PREFS[:log_file]] = file
119
129
  end
120
130
 
121
- #
122
- # Add the extension (directory, .zip or .xpi) at the given path to the profile.
123
- #
124
-
125
131
  def add_extension(path, name = extension_name_for(path))
132
+ WebDriver.logger.deprecate('Firefox::Profile#add_extension', 'Driver#install_addon',
133
+ id: :firefox_profile)
126
134
  @extensions[name] = Extension.new(path)
127
135
  end
128
136
 
@@ -133,7 +141,6 @@ module Selenium
133
141
  when :manual
134
142
  self['network.proxy.type'] = 1
135
143
 
136
- set_manual_proxy_preference 'ftp', proxy.ftp
137
144
  set_manual_proxy_preference 'http', proxy.http
138
145
  set_manual_proxy_preference 'ssl', proxy.ssl
139
146
  set_manual_proxy_preference 'socks', proxy.socks
@@ -26,14 +26,43 @@ module Selenium
26
26
  SHUTDOWN_SUPPORTED = false
27
27
  DRIVER_PATH_ENV_KEY = 'SE_GECKODRIVER'
28
28
 
29
- def initialize(path: nil, port: nil, log: nil, args: nil)
30
- args ||= []
29
+ def initialize(args: nil, **)
30
+ args = Array(args.dup)
31
31
  unless args.any? { |arg| arg.include?('--connect-existing') || arg.include?('--websocket-port') }
32
32
  args << '--websocket-port'
33
33
  args << '0'
34
34
  end
35
+
36
+ configure_debug_args(args) if ENV.key?('SE_DEBUG')
37
+
35
38
  super
36
39
  end
40
+
41
+ def launch
42
+ configure_debug_args(args) if ENV.key?('SE_DEBUG')
43
+
44
+ super
45
+ end
46
+
47
+ private
48
+
49
+ def configure_debug_args(args)
50
+ # An explicit geckodriver log setting is more specific than Selenium's
51
+ # generic SE_DEBUG fallback, so preserve it and skip adding `-v`.
52
+ if args.any? { |arg| arg.start_with?('--log') }
53
+ warn_explicit_log_preference unless @log_preference_warned
54
+ @log_preference_warned = true
55
+ args.reject! { |arg| arg.start_with?('-v') }
56
+ return
57
+ end
58
+
59
+ args << '-v' unless args.any? { |arg| /\A-v+\z/.match?(arg) }
60
+ end
61
+
62
+ def warn_explicit_log_preference
63
+ WebDriver.logger.warn('SE_DEBUG is set; preserving user-specified geckodriver --log setting instead of ' \
64
+ 'adding -v', id: :se_debug)
65
+ end
37
66
  end # Service
38
67
  end # Firefox
39
68
  end # WebDriver
@@ -31,9 +31,10 @@ module Selenium
31
31
 
32
32
  include LocalDriver
33
33
 
34
- def initialize(options: nil, service: nil, url: nil, **)
35
- caps, url = initialize_local_driver(options, service, url)
36
- super(caps: caps, url: url, **)
34
+ def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
35
+ initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
36
+ super(caps: caps, http_client: client, **)
37
+ end
37
38
  end
38
39
 
39
40
  def browser
@@ -25,6 +25,16 @@ module Selenium
25
25
  EXECUTABLE = 'IEDriverServer'
26
26
  SHUTDOWN_SUPPORTED = true
27
27
  DRIVER_PATH_ENV_KEY = 'SE_IEDRIVER'
28
+
29
+ def initialize(args: nil, **)
30
+ if ENV.key?('SE_DEBUG')
31
+ args = Array(args.dup)
32
+ warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
33
+ args << '--log-level=DEBUG'
34
+ end
35
+
36
+ super
37
+ end
28
38
  end # Server
29
39
  end # IE
30
40
  end # WebDriver
@@ -17,38 +17,60 @@
17
17
  # specific language governing permissions and limitations
18
18
  # under the License.
19
19
 
20
+ require 'selenium/webdriver/bidi'
21
+ require 'selenium/webdriver/bidi/protocol'
22
+
20
23
  module Selenium
21
24
  module WebDriver
22
25
  module Remote
23
26
  class BiDiBridge < Bridge
24
- attr_reader :bidi
27
+ attr_reader :bidi, :connection
28
+
29
+ READINESS_STATE = {
30
+ 'none' => :none,
31
+ 'eager' => :interactive,
32
+ 'normal' => :complete
33
+ }.freeze
25
34
 
26
35
  def create_session(capabilities)
27
36
  super
28
- socket_url = @capabilities[:web_socket_url]
29
- @bidi = Selenium::WebDriver::BiDi.new(url: socket_url)
37
+
38
+ begin
39
+ @bidi = Selenium::WebDriver::BiDi.new(url: validated_socket_url)
40
+ # Reuse the BiDi object's socket as the connection until the bridge owns it directly.
41
+ @connection = @bidi.ws
42
+ rescue StandardError
43
+ quit
44
+ raise
45
+ end
30
46
  end
31
47
 
32
48
  def get(url)
33
- browsing_context.navigate(url)
49
+ browsing_context.navigate(context: window_handle, url: url, wait: readiness_state)
50
+ nil
34
51
  end
35
52
 
36
53
  def go_back
37
- browsing_context.traverse_history(-1)
54
+ browsing_context.traverse_history(context: window_handle, delta: -1)
55
+ nil
38
56
  end
39
57
 
40
58
  def go_forward
41
- browsing_context.traverse_history(1)
59
+ browsing_context.traverse_history(context: window_handle, delta: 1)
60
+ nil
42
61
  end
43
62
 
44
63
  def refresh
45
- browsing_context.reload
64
+ browsing_context.reload(context: window_handle, wait: readiness_state)
65
+ nil
46
66
  end
47
67
 
48
68
  def quit
49
- super
69
+ bidi&.close
70
+ rescue *QUIT_ERRORS
71
+ nil
50
72
  ensure
51
- bidi.close
73
+ super
52
74
  end
53
75
 
54
76
  def close
@@ -57,8 +79,20 @@ module Selenium
57
79
 
58
80
  private
59
81
 
82
+ def validated_socket_url
83
+ url = @capabilities[:web_socket_url]
84
+ return url if url.is_a?(String) && url.start_with?('ws://', 'wss://')
85
+
86
+ raise Error::WebDriverError,
87
+ "BiDi was enabled, but the remote end did not return a valid webSocketUrl: #{url.inspect}."
88
+ end
89
+
60
90
  def browsing_context
61
- @browsing_context ||= WebDriver::BiDi::BrowsingContext.new(self)
91
+ @browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection)
92
+ end
93
+
94
+ def readiness_state
95
+ READINESS_STATE.fetch(capabilities[:page_load_strategy] || 'normal')
62
96
  end
63
97
  end # BiDiBridge
64
98
  end # Remote
@@ -51,20 +51,13 @@ module Selenium
51
51
  end
52
52
 
53
53
  #
54
- # Initializes the bridge with the given server URL
55
- # @param [String, URI] url url for the remote server
56
- # @param [Object] http_client an HTTP client instance that implements the same protocol as Http::Default
54
+ # @param [Object] http_client a configured HTTP client implementing the same protocol as Http::Default
57
55
  # @api private
58
56
  #
59
57
 
60
- def initialize(url:, http_client: nil)
61
- uri = url.is_a?(URI) ? url : URI.parse(url)
62
- uri.path += '/' unless uri.path.end_with?('/')
63
-
64
- @http = http_client || Http::Default.new
65
- @http.server_url = uri
58
+ def initialize(http_client:)
59
+ @http = http_client
66
60
  @file_detector = nil
67
-
68
61
  @locator_converter = self.class.locator_converter
69
62
  end
70
63
 
@@ -206,12 +199,20 @@ module Selenium
206
199
  switch_to_frame nil
207
200
  end
208
201
 
209
- QUIT_ERRORS = [IOError].freeze
202
+ QUIT_ERRORS = [IOError, EOFError, WebSocket::Error].freeze
210
203
 
211
204
  def quit
212
- execute :delete_session
213
- http.close
214
- rescue *QUIT_ERRORS
205
+ begin
206
+ execute :delete_session
207
+ rescue *QUIT_ERRORS => e
208
+ WebDriver.logger.debug "delete_session failed during quit: #{e.class}: #{e.message}", id: :quit
209
+ ensure
210
+ begin
211
+ http.close
212
+ rescue *QUIT_ERRORS => e
213
+ WebDriver.logger.debug "http.close failed during quit: #{e.class}: #{e.message}", id: :quit
214
+ end
215
+ end
215
216
  nil
216
217
  end
217
218
 
@@ -597,6 +598,11 @@ module Selenium
597
598
  raise(WebDriver::Error::WebDriverError, msg)
598
599
  end
599
600
 
601
+ def connection
602
+ msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
603
+ raise(WebDriver::Error::WebDriverError, msg)
604
+ end
605
+
600
606
  def command_list
601
607
  COMMANDS
602
608
  end
@@ -29,13 +29,18 @@ module Selenium
29
29
  include DriverExtensions::UploadsFiles
30
30
  include DriverExtensions::HasSessionId
31
31
  include DriverExtensions::HasFileDownloads
32
+ include DriverExtensions::HasSessionEvents
32
33
 
33
- def initialize(capabilities: nil, options: nil, service: nil, url: nil, **)
34
- raise ArgumentError, "Can not set :service object on #{self.class}" if service
34
+ def initialize(capabilities: nil, options: nil, service: nil, url: nil, http_client: nil, client_config: nil,
35
+ **)
36
+ assert_arguments(service, url, http_client, client_config)
35
37
 
36
- url ||= "http://#{Platform.localhost}:4444/wd/hub"
37
38
  caps = process_options(options, capabilities)
38
- super(caps: caps, url: url, **)
39
+ http_client ||= Remote::Http::Default.new(client_config: client_config)
40
+ server_url = url || client_config&.server_url || "http://#{Platform.localhost}:4444/wd/hub"
41
+ http_client.server_url = server_url
42
+ caps['se:remoteUrl'] = server_url.to_s.chomp('/')
43
+ super(caps: caps, http_client: http_client, **)
39
44
  @bridge.file_detector = ->((filename, *)) { File.exist?(filename) && filename.to_s }
40
45
  command_list = @bridge.command_list
41
46
  @bridge.extend(WebDriver::Remote::Features)
@@ -44,6 +49,12 @@ module Selenium
44
49
 
45
50
  private
46
51
 
52
+ def assert_arguments(service, url, http_client, client_config)
53
+ raise ArgumentError, "Can not set :service object on #{self.class}" if service
54
+ raise ArgumentError, 'Cannot use both :http_client and :client_config' if http_client && client_config
55
+ raise ArgumentError, 'Cannot use both :url and a ClientConfig#server_url' if url && client_config&.server_url
56
+ end
57
+
47
58
  def devtools_url
48
59
  capabilities['se:cdp']
49
60
  end
@@ -25,7 +25,8 @@ module Selenium
25
25
  upload_file: [:post, 'session/:session_id/se/file'],
26
26
  get_downloadable_files: [:get, 'session/:session_id/se/files'],
27
27
  download_file: [:post, 'session/:session_id/se/files'],
28
- delete_downloadable_files: [:delete, 'session/:session_id/se/files']
28
+ delete_downloadable_files: [:delete, 'session/:session_id/se/files'],
29
+ fire_session_event: [:post, 'session/:session_id/se/event']
29
30
  }.freeze
30
31
 
31
32
  def add_commands(commands)
@@ -69,6 +70,30 @@ module Selenium
69
70
  def delete_downloadable_files
70
71
  execute :delete_downloadable_files
71
72
  end
73
+
74
+ #
75
+ # Fires a custom session event to the remote server event bus.
76
+ # This allows test code to trigger server-side utilities that subscribe to
77
+ # the event bus.
78
+ #
79
+ # @param [String] event_type The type of event (e.g., "test:failed", "log:collect")
80
+ # @param [Hash] payload Optional data to include with the event
81
+ # @return [Hash] Response data including success status, event type, and timestamp
82
+ #
83
+ # @example Fire a simple event
84
+ # driver.fire_session_event("test:started")
85
+ #
86
+ # @example Fire an event with payload
87
+ # driver.fire_session_event("test:failed", {
88
+ # testName: "LoginTest",
89
+ # error: "Element not found"
90
+ # })
91
+ #
92
+ def fire_session_event(event_type, payload = nil)
93
+ params = {eventType: event_type}
94
+ params[:payload] = payload if payload
95
+ execute :fire_session_event, {}, params
96
+ end
72
97
  end
73
98
  end # Remote
74
99
  end # WebDriver
@@ -28,17 +28,35 @@ module Selenium
28
28
  'Accept' => CONTENT_TYPE,
29
29
  'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8"
30
30
  }.freeze
31
+ BINARY_ENCODINGS = [Encoding::BINARY, Encoding::ASCII_8BIT].freeze
31
32
 
32
33
  class << self
33
- attr_accessor :extra_headers
34
- attr_writer :user_agent
35
-
36
34
  def user_agent
37
- @user_agent ||= "selenium/#{WebDriver::VERSION} (ruby #{Platform.os})"
35
+ ClientConfig.default_user_agent
36
+ end
37
+
38
+ def user_agent=(value)
39
+ ClientConfig.default_user_agent = value
40
+ end
41
+
42
+ def extra_headers
43
+ ClientConfig.default_extra_headers
44
+ end
45
+
46
+ def extra_headers=(value)
47
+ ClientConfig.default_extra_headers = value
38
48
  end
39
49
  end
40
50
 
41
- attr_writer :server_url
51
+ attr_reader :client_config
52
+
53
+ def initialize(client_config: nil)
54
+ @client_config = client_config || ClientConfig.new
55
+ end
56
+
57
+ def server_url=(url)
58
+ client_config.server_url = url
59
+ end
42
60
 
43
61
  def quit_errors
44
62
  [IOError]
@@ -55,6 +73,7 @@ module Selenium
55
73
  headers['Cache-Control'] = 'no-cache' if verb == :get
56
74
 
57
75
  if command_hash
76
+ command_hash = ensure_utf8_encoding(command_hash)
58
77
  payload = JSON.generate(command_hash)
59
78
  headers['Content-Length'] = payload.bytesize.to_s if %i[post put].include?(verb)
60
79
 
@@ -74,23 +93,49 @@ module Selenium
74
93
  def common_headers
75
94
  @common_headers ||= begin
76
95
  headers = DEFAULT_HEADERS.dup
77
- headers['User-Agent'] = Common.user_agent
78
- headers = headers.merge(Common.extra_headers || {})
79
-
80
- headers
96
+ headers['User-Agent'] = client_config.user_agent
97
+ headers.merge(client_config.extra_headers || {})
81
98
  end
82
99
  end
83
100
 
84
101
  def server_url
85
- return @server_url if @server_url
86
-
87
- raise Error::WebDriverError, 'server_url not set'
102
+ client_config.server_url || raise(Error::WebDriverError, 'server_url not set')
88
103
  end
89
104
 
90
105
  def request(*)
91
106
  raise NotImplementedError, 'subclass responsibility'
92
107
  end
93
108
 
109
+ def ensure_utf8_encoding(obj)
110
+ case obj
111
+ when String
112
+ encode_string_to_utf8(obj)
113
+ when Array
114
+ obj.map { |item| ensure_utf8_encoding(item) }
115
+ when Hash
116
+ obj.each_with_object({}) do |(key, value), result|
117
+ result[ensure_utf8_encoding(key)] = ensure_utf8_encoding(value)
118
+ end
119
+ else
120
+ obj
121
+ end
122
+ end
123
+
124
+ def encode_string_to_utf8(str)
125
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
126
+
127
+ if BINARY_ENCODINGS.include?(str.encoding)
128
+ result = str.dup.force_encoding(Encoding::UTF_8)
129
+ return result if result.valid_encoding?
130
+ end
131
+
132
+ str.encode(Encoding::UTF_8)
133
+ rescue EncodingError => e
134
+ raise Error::WebDriverError,
135
+ "Unable to encode string to UTF-8: #{e.message}. " \
136
+ "String encoding: #{str.encoding}, content: #{str.inspect}"
137
+ end
138
+
94
139
  def create_response(code, body, content_type)
95
140
  code = code.to_i
96
141
  body = body.to_s.strip
@@ -26,20 +26,18 @@ module Selenium
26
26
  #
27
27
  # An alternative to the default Net::HTTP client.
28
28
  #
29
- # This can be used for the Firefox and Remote drivers if you have Curb
30
- # installed.
31
- #
32
- # @example Using Curb
33
- # require 'selenium/webdriver/remote/http/curb'
34
- # include Selenium
35
- #
36
- # driver = WebDriver.for :firefox, :http_client => WebDriver::Remote::Http::Curb.new
29
+ # @deprecated Subclass {Selenium::WebDriver::Remote::Http::Common} to implement a custom HTTP client.
37
30
  #
38
31
 
39
32
  class Curb < Common
40
33
  attr_accessor :timeout
41
34
 
42
35
  def initialize(timeout: nil)
36
+ WebDriver.logger.deprecate(
37
+ 'Selenium::WebDriver::Remote::Http::Curb',
38
+ 'a custom subclass of Selenium::WebDriver::Remote::Http::Common',
39
+ id: :curb
40
+ )
43
41
  @timeout = timeout
44
42
  super()
45
43
  end
@@ -24,19 +24,41 @@ module Selenium
24
24
  module Http
25
25
  # @api private
26
26
  class Default < Common
27
- attr_writer :proxy
28
-
29
- attr_accessor :open_timeout, :read_timeout
30
-
31
27
  # Initializes object.
32
28
  # Warning: Setting {#open_timeout} to non-nil values will cause a separate thread to spawn.
33
29
  # Debuggers that freeze the process will not be able to evaluate any operations if that happens.
30
+ # @param [ClientConfig] client_config - Configuration used to build the HTTP client.
34
31
  # @param [Numeric] open_timeout - Open timeout to apply to HTTP client.
35
32
  # @param [Numeric] read_timeout - Read timeout (seconds) to apply to HTTP client.
36
- def initialize(open_timeout: nil, read_timeout: nil)
37
- @open_timeout = open_timeout
38
- @read_timeout = read_timeout
39
- super()
33
+ def initialize(client_config: nil, open_timeout: nil, read_timeout: nil)
34
+ if client_config && (open_timeout || read_timeout)
35
+ raise ArgumentError, 'Cannot use both :client_config and :open_timeout/:read_timeout'
36
+ end
37
+
38
+ client_config ||= ClientConfig.new
39
+ client_config.open_timeout = open_timeout if open_timeout
40
+ client_config.read_timeout = read_timeout if read_timeout
41
+ super(client_config: client_config)
42
+ end
43
+
44
+ def open_timeout
45
+ client_config.open_timeout
46
+ end
47
+
48
+ def read_timeout
49
+ client_config.read_timeout
50
+ end
51
+
52
+ def open_timeout=(value)
53
+ client_config.open_timeout = value
54
+ end
55
+
56
+ def read_timeout=(value)
57
+ client_config.read_timeout = value
58
+ end
59
+
60
+ def proxy=(value)
61
+ client_config.proxy = value
40
62
  end
41
63
 
42
64
  def close
@@ -94,16 +116,20 @@ module Selenium
94
116
  end
95
117
 
96
118
  if response.is_a? Net::HTTPRedirection
97
- WebDriver.logger.debug("Redirect to #{response['Location']}; times: #{redirects}")
98
- raise Error::WebDriverError, 'too many redirects' if redirects >= MAX_REDIRECTS
99
-
100
- request(:get, URI.parse(response['Location']), DEFAULT_HEADERS.dup, nil, redirects + 1)
119
+ follow_redirect(response, redirects)
101
120
  else
102
121
  WebDriver.logger.debug(" <<< #{response.instance_variable_get(:@header).inspect}", id: :header)
103
122
  create_response response.code, response.body, response.content_type
104
123
  end
105
124
  end
106
125
 
126
+ def follow_redirect(response, redirects)
127
+ WebDriver.logger.debug("Redirect to #{response['Location']}; times: #{redirects}", id: :redirect)
128
+ raise Error::WebDriverError, 'too many redirects' if redirects >= client_config.max_redirects
129
+
130
+ request(:get, URI.parse(response['Location']), DEFAULT_HEADERS.dup, nil, redirects + 1)
131
+ end
132
+
107
133
  def new_request_for(verb, url, headers, payload)
108
134
  req = Net::HTTP.const_get(verb.to_s.capitalize).new(url.path, headers)
109
135
 
@@ -120,52 +146,43 @@ module Selenium
120
146
 
121
147
  def new_http_client
122
148
  if use_proxy?
123
- url = @proxy.http
149
+ url = proxy.http
124
150
  unless proxy.respond_to?(:http) && url
125
151
  raise Error::WebDriverError,
126
- "expected HTTP proxy, got #{@proxy.inspect}"
152
+ "expected HTTP proxy, got #{proxy.inspect}"
127
153
  end
128
154
 
129
- proxy = URI.parse(url)
155
+ proxy_uri = URI.parse(url)
130
156
 
131
- Net::HTTP.new(server_url.host, server_url.port, proxy.host, proxy.port, proxy.user, proxy.password)
157
+ Net::HTTP.new(server_url.host, server_url.port,
158
+ proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
132
159
  else
133
160
  Net::HTTP.new server_url.host, server_url.port
134
161
  end
135
162
  end
136
163
 
137
164
  def proxy
138
- @proxy ||= begin
139
- proxy = ENV.fetch('http_proxy', nil) || ENV.fetch('HTTP_PROXY', nil)
140
- no_proxy = ENV.fetch('no_proxy', nil) || ENV.fetch('NO_PROXY', nil)
141
-
142
- if proxy
143
- proxy = "http://#{proxy}" unless proxy.start_with?('http://')
144
- Proxy.new(http: proxy, no_proxy: no_proxy)
145
- end
146
- end
165
+ client_config.proxy
147
166
  end
148
167
 
149
168
  def use_proxy?
150
169
  return false if proxy.nil?
170
+ return true unless proxy.no_proxy
151
171
 
152
- if proxy.no_proxy
153
- ignored = proxy.no_proxy.split(',').any? do |host|
154
- host == '*' ||
155
- host == server_url.host || (
156
- begin
157
- IPAddr.new(host).include?(server_url.host)
158
- rescue ArgumentError
159
- false
160
- end
161
- )
162
- end
172
+ !proxy_ignored?
173
+ end
163
174
 
164
- !ignored
165
- else
166
- true
175
+ def proxy_ignored?
176
+ proxy.no_proxy.split(',').map(&:strip).reject(&:empty?).any? do |host|
177
+ host == '*' || host == server_url.host || ip_match?(host)
167
178
  end
168
179
  end
180
+
181
+ def ip_match?(host)
182
+ IPAddr.new(host).include?(server_url.host)
183
+ rescue ArgumentError
184
+ false
185
+ end
169
186
  end # Default
170
187
  end # Http
171
188
  end # Remote
@@ -31,9 +31,10 @@ module Selenium
31
31
 
32
32
  include LocalDriver
33
33
 
34
- def initialize(options: nil, service: nil, url: nil, **)
35
- caps, url = initialize_local_driver(options, service, url)
36
- super(caps: caps, url: url, **)
34
+ def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
35
+ initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
36
+ super(caps: caps, http_client: client, **)
37
+ end
37
38
  end
38
39
 
39
40
  def browser