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
@@ -55,9 +55,32 @@ module Selenium
55
55
  @ignored = Array(ignored)
56
56
  @allowed = Array(allowed)
57
57
  @first_warning = false
58
+ @level_forced = false
59
+ @output_forced = false
60
+ end
61
+
62
+ #
63
+ # Forces debug level and prevents it from being overridden.
64
+ #
65
+ def debug!
66
+ @level_forced = true
67
+ @logger.level = :debug
68
+ end
69
+
70
+ #
71
+ # Forces output to stderr and prevents it from being overridden.
72
+ #
73
+ def stderr!
74
+ @output_forced = true
75
+ @logger.reopen($stderr)
58
76
  end
59
77
 
60
78
  def level=(level)
79
+ if @level_forced
80
+ warn('Logger level is forced; ignoring override', id: :logger)
81
+ return
82
+ end
83
+
61
84
  if level == :info && @logger.level == :info
62
85
  info(':info is now the default log level, to see additional logging, set log level to :debug')
63
86
  end
@@ -71,6 +94,11 @@ module Selenium
71
94
  # @param [String] io
72
95
  #
73
96
  def output=(io)
97
+ if @output_forced
98
+ warn('Logger output is forced; ignoring override', id: :logger)
99
+ return
100
+ end
101
+
74
102
  @logger.reopen(io)
75
103
  end
76
104
 
@@ -79,6 +79,8 @@ module Selenium
79
79
  #
80
80
 
81
81
  def delete_cookie(name)
82
+ raise ArgumentError, 'Cookie name cannot be null or empty' if name.nil? || name.to_s.strip.empty?
83
+
82
84
  @bridge.delete_cookie name
83
85
  end
84
86
 
@@ -71,8 +71,12 @@ module Selenium
71
71
  def initialize(**opts)
72
72
  self.class.set_capabilities
73
73
 
74
+ opts[:web_socket_url] = opts.delete(:bidi) if opts.key?(:bidi)
75
+
74
76
  @options = opts
75
77
  @options[:browser_name] = self.class::BROWSER
78
+
79
+ enable_bidi! if @options[:web_socket_url]
76
80
  end
77
81
 
78
82
  #
@@ -91,6 +95,20 @@ module Selenium
91
95
  @options[name] = value
92
96
  end
93
97
 
98
+ #
99
+ # Enables WebDriver BiDi by requesting the W3C webSocketUrl capability.
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+
104
+ def enable_bidi!
105
+ @options[:web_socket_url] = true
106
+ end
107
+
108
+ def bidi?
109
+ !!@options[:web_socket_url]
110
+ end
111
+
94
112
  def ==(other)
95
113
  return false unless other.is_a? self.class
96
114
 
@@ -120,22 +138,43 @@ module Selenium
120
138
  browser_options = {self.class::KEY => browser_options} if defined?(self.class::KEY)
121
139
 
122
140
  process_browser_options(browser_options)
123
- generate_as_json(w3c_options.merge(browser_options))
141
+ generate_as_json(merge_browser_options(w3c_options, browser_options))
124
142
  end
125
143
 
126
144
  private
127
145
 
146
+ # Preserve a hand-built vendor options hash (e.g. passed through #add_option) by merging it
147
+ # with the binding's own, rather than letting one silently overwrite the other.
148
+ def merge_browser_options(w3c_options, browser_options)
149
+ w3c_options.merge(browser_options) do |_key, w3c_value, browser_value|
150
+ if w3c_value.is_a?(Hash) && browser_value.is_a?(Hash)
151
+ browser_value.merge(w3c_value)
152
+ else
153
+ browser_value
154
+ end
155
+ end
156
+ end
157
+
128
158
  def w3c?(key)
129
159
  W3C_OPTIONS.include?(key) || key.to_s.include?(':')
130
160
  end
131
161
 
132
162
  def process_w3c_options(options)
133
163
  w3c_options = options.select { |key, val| w3c?(key) && !val.nil? }
134
- w3c_options[:unhandled_prompt_behavior] &&= w3c_options[:unhandled_prompt_behavior]&.to_s&.tr('_', ' ')
164
+ w3c_options[:unhandled_prompt_behavior] &&=
165
+ process_unhandled_prompt_behavior_value(w3c_options[:unhandled_prompt_behavior])
135
166
  options.delete_if { |key, _val| w3c?(key) }
136
167
  w3c_options
137
168
  end
138
169
 
170
+ def process_unhandled_prompt_behavior_value(value)
171
+ if value.is_a?(Hash)
172
+ value.transform_values { |v| process_unhandled_prompt_behavior_value(v) }
173
+ else
174
+ value&.to_s&.tr('_', ' ')
175
+ end
176
+ end
177
+
139
178
  def process_browser_options(_browser_options)
140
179
  nil
141
180
  end
@@ -51,9 +51,7 @@ module Selenium
51
51
  end
52
52
 
53
53
  def ci
54
- if ENV['TRAVIS']
55
- :travis
56
- elsif ENV['JENKINS']
54
+ if ENV['JENKINS']
57
55
  :jenkins
58
56
  elsif ENV['APPVEYOR']
59
57
  :appveyor
@@ -29,7 +29,6 @@ module Selenium
29
29
  }.freeze
30
30
 
31
31
  ALLOWED = {type: 'proxyType',
32
- ftp: 'ftpProxy',
33
32
  http: 'httpProxy',
34
33
  no_proxy: 'noProxy',
35
34
  pac: 'proxyAutoconfigUrl',
@@ -76,12 +75,6 @@ module Selenium
76
75
  end
77
76
  alias eql? ==
78
77
 
79
- def ftp=(value)
80
- WebDriver.logger.deprecate('FTP proxy support', nil, id: :ftp_proxy)
81
- self.type = :manual
82
- @ftp = value
83
- end
84
-
85
78
  def http=(value)
86
79
  self.type = :manual
87
80
  @http = value
@@ -143,9 +136,8 @@ module Selenium
143
136
  def as_json(*)
144
137
  json_result = {
145
138
  'proxyType' => TYPES[type].downcase,
146
- 'ftpProxy' => ftp,
147
139
  'httpProxy' => http,
148
- 'noProxy' => no_proxy.is_a?(String) ? no_proxy.split(', ') : no_proxy,
140
+ 'noProxy' => no_proxy.is_a?(String) ? no_proxy.split(',').map(&:strip).reject(&:empty?) : no_proxy,
149
141
  'proxyAutoconfigUrl' => pac,
150
142
  'sslProxy' => ssl,
151
143
  'autodetect' => auto_detect,
@@ -50,7 +50,8 @@ module Selenium
50
50
  def binary
51
51
  @binary ||= begin
52
52
  if (location = ENV.fetch('SE_MANAGER_PATH', nil))
53
- WebDriver.logger.debug("Selenium Manager set by ENV['SE_MANAGER_PATH']: #{location}")
53
+ WebDriver.logger.debug("Selenium Manager set by ENV['SE_MANAGER_PATH']: #{location}",
54
+ id: :selenium_manager)
54
55
  end
55
56
  location ||= platform_location
56
57
 
@@ -69,7 +69,6 @@ module Selenium
69
69
  def initialize(path: nil, port: nil, log: nil, args: nil)
70
70
  port ||= self.class::DEFAULT_PORT
71
71
  args ||= []
72
- path ||= env_path
73
72
 
74
73
  @executable_path = path
75
74
  @host = Platform.localhost
@@ -88,7 +87,7 @@ module Selenium
88
87
  end
89
88
 
90
89
  def launch
91
- @executable_path ||= env_path || find_driver_path
90
+ @executable_path ||= DriverFinder.new(nil, self).driver_path
92
91
  ServiceManager.new(self).tap(&:start)
93
92
  end
94
93
 
@@ -96,13 +95,10 @@ module Selenium
96
95
  self.class::SHUTDOWN_SUPPORTED
97
96
  end
98
97
 
99
- def find_driver_path
100
- default_options = WebDriver.const_get("#{self.class.name&.split('::')&.[](2)}::Options").new
101
- DriverFinder.new(default_options, self).driver_path
102
- end
98
+ private
103
99
 
104
- def env_path
105
- ENV.fetch(self.class::DRIVER_PATH_ENV_KEY, nil)
100
+ def warn_driver_log_override
101
+ WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver logging settings', id: :se_debug)
106
102
  end
107
103
  end # Service
108
104
  end # WebDriver
@@ -80,7 +80,13 @@ module Selenium
80
80
  def build_process(*command)
81
81
  WebDriver.logger.debug("Executing Process #{command}", id: :driver_service)
82
82
  @process = ChildProcess.build(*command)
83
- @io ||= WebDriver.logger.io if WebDriver.logger.debug?
83
+ if ENV.key?('SE_DEBUG')
84
+ if @io && @io != WebDriver.logger.io
85
+ WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver log output to use stderr',
86
+ id: :se_debug)
87
+ end
88
+ @io = WebDriver.logger.io
89
+ end
84
90
  @process.io = @io if @io
85
91
 
86
92
  @process
@@ -127,10 +133,36 @@ module Selenium
127
133
  end
128
134
 
129
135
  def connect_until_stable
130
- socket_poller = SocketPoller.new @host, @port, START_TIMEOUT
131
- return if socket_poller.connected?
136
+ deadline = current_time + START_TIMEOUT
137
+
138
+ loop do
139
+ error = check_connection_error
140
+ return unless error
141
+
142
+ raise Error::WebDriverError, "#{cannot_connect_error_text}: #{error}" if current_time > deadline
143
+
144
+ sleep 0.1
145
+ end
146
+ end
147
+
148
+ def check_connection_error
149
+ response = Net::HTTP.start(@host, @port, open_timeout: 0.5, read_timeout: 1) do |http|
150
+ http.get('/status', {'Connection' => 'close'})
151
+ end
152
+
153
+ return "status returned #{response.code}\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)
154
+
155
+ status = JSON.parse(response.body)
156
+ ready = status['ready'] || status.dig('value', 'ready')
157
+ "driver not ready: #{response.body}" unless ready
158
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
159
+ Errno::EADDRNOTAVAIL, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
160
+ EOFError, SocketError, Net::HTTPBadResponse, JSON::ParserError => e
161
+ "#{e.class}: #{e.message}"
162
+ end
132
163
 
133
- raise Error::WebDriverError, cannot_connect_error_text
164
+ def current_time
165
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
166
  end
135
167
 
136
168
  def cannot_connect_error_text
@@ -93,7 +93,7 @@ module Selenium
93
93
  true
94
94
  rescue *NOT_CONNECTED_ERRORS
95
95
  sock&.close
96
- WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :driver_service)
96
+ WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :socket_poller)
97
97
  false
98
98
  end
99
99
  end
@@ -36,7 +36,7 @@ module Selenium
36
36
  'It should end with .png extension',
37
37
  id: :screenshot
38
38
  end
39
- WebDriver.logger.debug("Saving screenshot to #{Dir.pwd}/#{png_path}")
39
+ WebDriver.logger.debug("Saving screenshot to #{Dir.pwd}/#{png_path}", id: :screenshot)
40
40
  File.open(png_path, 'wb') { |f| f << screenshot_as(:png, full_page: full_page) }
41
41
  end
42
42
 
@@ -47,7 +47,7 @@ module Selenium
47
47
  new(id: decode(opts['credentialId']),
48
48
  resident_credential: opts['isResidentCredential'],
49
49
  rp_id: opts['rpId'],
50
- private_key: opts['privateKey'],
50
+ private_key: decode(opts['privateKey']),
51
51
  sign_count: opts['signCount'],
52
52
  user_handle: user_handle)
53
53
  end
@@ -37,7 +37,8 @@ module Selenium
37
37
  @timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT)
38
38
  @interval = opts.fetch(:interval, DEFAULT_INTERVAL)
39
39
  @message = opts[:message]
40
- @ignored = Array(opts[:ignore] || Error::NoSuchElementError)
40
+ @message_provider = opts[:message_provider]
41
+ @ignored = Array(opts[:ignore] || Error::NoSuchElementError)
41
42
  end
42
43
 
43
44
  #
@@ -64,6 +65,8 @@ module Selenium
64
65
 
65
66
  msg = if @message
66
67
  @message.dup
68
+ elsif @message_provider
69
+ @message_provider.call
67
70
  else
68
71
  "timed out after #{@timeout} seconds"
69
72
  end
@@ -24,7 +24,10 @@ module Selenium
24
24
  class WebSocketConnection
25
25
  CONNECTION_ERRORS = [
26
26
  Errno::ECONNRESET, # connection is aborted (browser process was killed)
27
- Errno::EPIPE # broken pipe (browser process was killed)
27
+ Errno::EPIPE, # broken pipe (browser process was killed)
28
+ Errno::EBADF, # file descriptor already closed (double-close or GC)
29
+ IOError, # Ruby socket read/write after close
30
+ EOFError # socket reached EOF after remote closed cleanly
28
31
  ].freeze
29
32
 
30
33
  RESPONSE_WAIT_TIMEOUT = 30
@@ -35,6 +38,11 @@ module Selenium
35
38
  def initialize(url:)
36
39
  @callback_threads = ThreadGroup.new
37
40
 
41
+ @callbacks_mtx = Mutex.new
42
+ @messages_mtx = Mutex.new
43
+ @closing_mtx = Mutex.new
44
+
45
+ @closing = false
38
46
  @session_id = nil
39
47
  @url = url
40
48
 
@@ -43,9 +51,26 @@ module Selenium
43
51
  end
44
52
 
45
53
  def close
46
- @callback_threads.list.each(&:exit)
47
- @socket_thread.exit
48
- socket.close
54
+ @closing_mtx.synchronize do
55
+ return if @closing
56
+
57
+ @closing = true
58
+ end
59
+
60
+ begin
61
+ socket.close
62
+ rescue *CONNECTION_ERRORS => e
63
+ WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
64
+ # already closed
65
+ end
66
+
67
+ # Let threads unwind instead of calling exit
68
+ @socket_thread&.join(0.5)
69
+ @callback_threads.list.each do |thread|
70
+ thread.join(0.5)
71
+ rescue StandardError => e
72
+ WebDriver.logger.debug "Failed to join thread during close: #{e.class}: #{e.message}", id: :ws
73
+ end
49
74
  end
50
75
 
51
76
  def callbacks
@@ -53,62 +78,73 @@ module Selenium
53
78
  end
54
79
 
55
80
  def add_callback(event, &block)
56
- callbacks[event] << block
57
- block.object_id
81
+ @callbacks_mtx.synchronize do
82
+ callbacks[event] << block
83
+ block.object_id
84
+ end
58
85
  end
59
86
 
60
87
  def remove_callback(event, id)
61
- return if callbacks[event].reject! { |callback| callback.object_id == id }
88
+ @callbacks_mtx.synchronize do
89
+ return if @closing
90
+
91
+ callbacks_for_event = callbacks[event]
92
+ return if callbacks_for_event.reject! { |cb| cb.object_id == id }
62
93
 
63
- ids = callbacks[event]&.map(&:object_id)
64
- raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
94
+ ids = callbacks_for_event.map(&:object_id)
95
+ raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
96
+ end
65
97
  end
66
98
 
67
99
  def send_cmd(**payload)
68
100
  id = next_id
69
101
  data = payload.merge(id: id)
70
- WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
102
+ WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
71
103
  data = JSON.generate(data)
72
104
  out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
73
- socket.write(out_frame.to_s)
74
105
 
75
- wait.until { messages.delete(id) }
106
+ begin
107
+ socket.write(out_frame.to_s)
108
+ rescue *CONNECTION_ERRORS => e
109
+ raise e, "WebSocket is closed (#{e.class}: #{e.message})"
110
+ end
111
+
112
+ wait.until { @messages_mtx.synchronize { messages.delete(id) } }
76
113
  end
77
114
 
78
115
  private
79
116
 
80
- # We should be thread-safe to use the hash without synchronization
81
- # because its keys are WebSocket message identifiers and they should be
82
- # unique within a devtools session.
83
117
  def messages
84
118
  @messages ||= {}
85
119
  end
86
120
 
87
121
  def process_handshake
88
122
  socket.print(ws.to_s)
89
- ws << socket.readpartial(1024)
123
+ ws << socket.readpartial(1024) until ws.finished?
90
124
  end
91
125
 
92
126
  def attach_socket_listener
93
127
  Thread.new do
94
- Thread.current.abort_on_exception = true
95
128
  Thread.current.report_on_exception = false
96
129
 
97
- until socket.eof?
130
+ loop do
131
+ break if @closing
132
+
98
133
  incoming_frame << socket.readpartial(1024)
99
134
 
100
135
  while (frame = incoming_frame.next)
136
+ break if @closing
137
+
101
138
  message = process_frame(frame)
102
139
  next unless message['method']
103
140
 
104
- params = message['params']
105
- callbacks[message['method']].each do |callback|
106
- @callback_threads.add(callback_thread(params, &callback))
141
+ @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
142
+ @callback_threads.add(callback_thread(message['params'], &callback))
107
143
  end
108
144
  end
109
145
  end
110
- rescue *CONNECTION_ERRORS
111
- Thread.stop
146
+ rescue *CONNECTION_ERRORS, WebSocket::Error => e
147
+ WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
112
148
  end
113
149
  end
114
150
 
@@ -122,27 +158,27 @@ module Selenium
122
158
  # Firefox will periodically fail on unparsable empty frame
123
159
  return {} if message.empty?
124
160
 
125
- message = JSON.parse(message)
126
- messages[message['id']] = message
127
- WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
161
+ msg = JSON.parse(message)
162
+ @messages_mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') }
128
163
 
129
- message
164
+ WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
165
+ msg
130
166
  end
131
167
 
132
168
  def callback_thread(params)
133
169
  Thread.new do
134
- Thread.current.abort_on_exception = true
135
-
136
- # We might end up blocked forever when we have an error in event.
137
- # For example, if network interception event raises error,
138
- # the browser will keep waiting for the request to be proceeded
139
- # before returning back to the original thread. In this case,
140
- # we should at least print the error.
141
- Thread.current.report_on_exception = true
170
+ Thread.current.abort_on_exception = false
171
+ Thread.current.report_on_exception = false
172
+ next if @closing
142
173
 
143
174
  yield params
144
- rescue Error::WebDriverError, *CONNECTION_ERRORS
145
- Thread.stop
175
+ rescue Error::WebDriverError, *CONNECTION_ERRORS => e
176
+ WebDriver.logger.debug "Callback aborted: #{e.class}: #{e.message}", id: :ws
177
+ rescue StandardError => e
178
+ next if @closing
179
+
180
+ bt = Array(e.backtrace).first(5).join("\n")
181
+ WebDriver.logger.error "Callback error: #{e.class}: #{e.message}\n#{bt}", id: :ws
146
182
  end
147
183
  end
148
184
 
@@ -18,6 +18,7 @@
18
18
  # under the License.
19
19
 
20
20
  require 'selenium/webdriver/common/error'
21
+ require 'selenium/webdriver/common/client_config'
21
22
  require 'selenium/webdriver/common/local_driver'
22
23
  require 'selenium/webdriver/common/driver_finder'
23
24
  require 'selenium/webdriver/common/platform'
@@ -75,9 +76,9 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
75
76
  require 'selenium/webdriver/common/driver_extensions/uploads_files'
76
77
  require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
77
78
  require 'selenium/webdriver/common/driver_extensions/has_addons'
78
- require 'selenium/webdriver/common/driver_extensions/has_bidi'
79
79
  require 'selenium/webdriver/common/driver_extensions/has_devtools'
80
80
  require 'selenium/webdriver/common/driver_extensions/has_file_downloads'
81
+ require 'selenium/webdriver/common/driver_extensions/has_session_events'
81
82
  require 'selenium/webdriver/common/driver_extensions/has_authentication'
82
83
  require 'selenium/webdriver/common/driver_extensions/has_logs'
83
84
  require 'selenium/webdriver/common/driver_extensions/has_log_events'
@@ -34,9 +34,9 @@ module Selenium
34
34
  id: id,
35
35
  code: params['responseStatusCode'],
36
36
  body: (Base64.strict_decode64(encoded_body) if encoded_body),
37
- headers: params.fetch('responseHeaders', []).each_with_object({}) do |header, hash|
38
- hash[header['name']] = header['value']
39
- end
37
+ headers: params.fetch('responseHeaders', []).to_h do |header|
38
+ [header['name'], header['value']]
39
+ end
40
40
  )
41
41
  end
42
42
 
@@ -84,7 +84,7 @@ module Selenium
84
84
  def start_session(target_type:)
85
85
  targets = target.get_targets.dig('result', 'targetInfos')
86
86
  found_target = targets.find { |target| target['type'] == target_type }
87
- raise Error::WebDriverError, "Target type '#{target_type}' not found" unless found_target
87
+ raise Error::NoSuchTargetError, "Target type '#{target_type}' not found" unless found_target
88
88
 
89
89
  session = target.attach_to_target(target_id: found_target['targetId'], flatten: true)
90
90
  @session_id = session.dig('result', 'sessionId')
@@ -30,9 +30,10 @@ module Selenium
30
30
  class Driver < Chromium::Driver
31
31
  include LocalDriver
32
32
 
33
- def initialize(options: nil, service: nil, url: nil, **)
34
- caps, url = initialize_local_driver(options, service, url)
35
- super(caps: caps, url: url, **)
33
+ def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
34
+ initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
35
+ super(caps: caps, http_client: client, **)
36
+ end
36
37
  end
37
38
 
38
39
  def browser
@@ -25,6 +25,20 @@ module Selenium
25
25
  EXECUTABLE = 'msedgedriver'
26
26
  SHUTDOWN_SUPPORTED = true
27
27
  DRIVER_PATH_ENV_KEY = 'SE_EDGEDRIVER'
28
+
29
+ def initialize(args: nil, **)
30
+ args = Array(args.dup)
31
+ # yes, it is --enable-chrome-logs, even on msedgedriver; skip when CHROME_LOG_FILE is set
32
+ args << '--enable-chrome-logs' unless ENV.key?('CHROME_LOG_FILE') || args.include?('--enable-chrome-logs')
33
+
34
+ if ENV.key?('SE_DEBUG')
35
+ warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
36
+ args << '--verbose'
37
+ end
38
+
39
+ super
40
+ end
41
+
28
42
  def log
29
43
  return @log unless @log.is_a? String
30
44
 
@@ -29,16 +29,14 @@ module Selenium
29
29
  EXTENSIONS = [DriverExtensions::HasAddons,
30
30
  DriverExtensions::FullPageScreenshot,
31
31
  DriverExtensions::HasContext,
32
- DriverExtensions::HasBiDi,
33
- DriverExtensions::HasLogEvents,
34
- DriverExtensions::HasNetworkInterception,
35
32
  DriverExtensions::PrintsPage].freeze
36
33
 
37
34
  include LocalDriver
38
35
 
39
- def initialize(options: nil, service: nil, url: nil, **)
40
- caps, url = initialize_local_driver(options, service, url)
41
- super(caps: caps, url: url, **)
36
+ def initialize(options: nil, service: nil, url: nil, http_client: nil, client_config: nil, **)
37
+ initialize_local_driver(options, service, url, http_client, client_config) do |caps, client|
38
+ super(caps: caps, http_client: client, **)
39
+ end
42
40
  end
43
41
 
44
42
  def browser
@@ -69,6 +69,7 @@ module Selenium
69
69
  @options[:prefs]['remote.active-protocols'] = 1
70
70
  @options[:env] ||= {}
71
71
  @options[:log] ||= {level: log_level} if log_level
72
+ @vendor_options = {}
72
73
 
73
74
  process_profile(@options.delete(:profile))
74
75
  end
@@ -102,6 +103,23 @@ module Selenium
102
103
  @options[:prefs][name] = value
103
104
  end
104
105
 
106
+ #
107
+ # Add a Firefox-specific capability nested in the `moz:firefoxOptions`
108
+ # object that is not yet exposed through a dedicated method. This is the
109
+ # escape hatch for legitimate vendor capabilities the bindings do not model.
110
+ #
111
+ # @example Set an option not handled by other methods
112
+ # options = Selenium::WebDriver::Firefox::Options.new
113
+ # options.add_firefox_option('unhandledCapability', 'value')
114
+ #
115
+ # @param [String, Symbol] name Name of the capability, as expected by geckodriver
116
+ # @param [Object] value Value of the capability
117
+ #
118
+
119
+ def add_firefox_option(name, value)
120
+ @vendor_options[name.to_s] = value
121
+ end
122
+
105
123
  #
106
124
  # Sets Firefox profile.
107
125
  #
@@ -153,6 +171,7 @@ module Selenium
153
171
  def process_browser_options(browser_options)
154
172
  browser_options['moz:debuggerAddress'] = true if @debugger_address
155
173
  options = browser_options[KEY]
174
+ options.merge!(@vendor_options)
156
175
  options['binary'] ||= Firefox.path if Firefox.path
157
176
  options['profile'] = @profile if @profile
158
177
  end