selenium-webdriver 4.46.0 → 4.48.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +39 -0
  3. data/bin/linux/selenium-manager +0 -0
  4. data/bin/macos/selenium-manager +0 -0
  5. data/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +5940 -0
  6. data/bin/selenium-manager.cdx.json +11708 -0
  7. data/bin/windows/selenium-manager.exe +0 -0
  8. data/lib/selenium/webdriver/bidi/error.rb +48 -0
  9. data/lib/selenium/webdriver/bidi/protocol/bluetooth.rb +53 -23
  10. data/lib/selenium/webdriver/bidi/protocol/browser.rb +43 -15
  11. data/lib/selenium/webdriver/bidi/protocol/browsing_context.rb +172 -68
  12. data/lib/selenium/webdriver/bidi/protocol/domain.rb +8 -2
  13. data/lib/selenium/webdriver/bidi/protocol/emulation.rb +123 -9
  14. data/lib/selenium/webdriver/bidi/protocol/error_code.rb +66 -0
  15. data/lib/selenium/webdriver/bidi/protocol/input.rb +101 -28
  16. data/lib/selenium/webdriver/bidi/protocol/log.rb +17 -6
  17. data/lib/selenium/webdriver/bidi/protocol/network.rb +146 -63
  18. data/lib/selenium/webdriver/bidi/protocol/permissions.rb +4 -2
  19. data/lib/selenium/webdriver/bidi/protocol/script.rb +264 -86
  20. data/lib/selenium/webdriver/bidi/protocol/session.rb +48 -3
  21. data/lib/selenium/webdriver/bidi/protocol/speculation.rb +4 -2
  22. data/lib/selenium/webdriver/bidi/protocol/storage.rb +32 -6
  23. data/lib/selenium/webdriver/bidi/protocol/user_agent_client_hints.rb +7 -2
  24. data/lib/selenium/webdriver/bidi/protocol/web_extension.rb +49 -10
  25. data/lib/selenium/webdriver/bidi/protocol.rb +3 -0
  26. data/lib/selenium/webdriver/bidi/serialization/record.rb +206 -33
  27. data/lib/selenium/webdriver/bidi/serialization/union.rb +50 -4
  28. data/lib/selenium/webdriver/bidi/serialization.rb +1 -1
  29. data/lib/selenium/webdriver/bidi/support/bidi_generate.rb +529 -45
  30. data/lib/selenium/webdriver/bidi/support/check_generated.rb +12 -4
  31. data/lib/selenium/webdriver/bidi/transport.rb +8 -3
  32. data/lib/selenium/webdriver/chrome/service.rb +4 -1
  33. data/lib/selenium/webdriver/chromium/driver.rb +0 -1
  34. data/lib/selenium/webdriver/chromium/options.rb +20 -0
  35. data/lib/selenium/webdriver/common/driver.rb +11 -0
  36. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +0 -7
  37. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +0 -7
  38. data/lib/selenium/webdriver/common/driver_finder.rb +4 -3
  39. data/lib/selenium/webdriver/common/options.rb +21 -1
  40. data/lib/selenium/webdriver/common/proxy.rb +0 -8
  41. data/lib/selenium/webdriver/common/selenium_manager.rb +2 -1
  42. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  43. data/lib/selenium/webdriver/common/websocket_connection.rb +64 -26
  44. data/lib/selenium/webdriver/common.rb +0 -1
  45. data/lib/selenium/webdriver/edge/service.rb +4 -1
  46. data/lib/selenium/webdriver/firefox/driver.rb +0 -3
  47. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  48. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  49. data/lib/selenium/webdriver/remote/bidi_bridge.rb +39 -12
  50. data/lib/selenium/webdriver/remote/bridge.rb +1 -1
  51. data/lib/selenium/webdriver/remote/driver.rb +3 -1
  52. data/lib/selenium/webdriver/remote/http/default.rb +1 -1
  53. data/lib/selenium/webdriver/safari/options.rb +16 -1
  54. data/lib/selenium/webdriver/support/guards/guard.rb +20 -3
  55. data/lib/selenium/webdriver/support/guards.rb +9 -3
  56. data/lib/selenium/webdriver/version.rb +1 -1
  57. metadata +6 -3
  58. data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +0 -36
@@ -25,21 +25,29 @@ module BiDiGenerate
25
25
  # current schema — catching a hand-edit or a forgotten regeneration. Re-renders each module
26
26
  # in memory (no file writes) and compares. The .rbs are covered by Steep.
27
27
  def self.check!(schema_rootpath)
28
- modules = build_ir(Schema.new(JSON.parse(File.read(schema_path(schema_rootpath)))))
28
+ schema = Schema.new(JSON.parse(File.read(schema_path(schema_rootpath))))
29
29
  protocol_dir = File.expand_path('../protocol', __dir__)
30
30
  template = File.join(__dir__, 'templates', 'module.rb.erb')
31
31
 
32
- stale = modules.reject do |mod|
32
+ stale = build_ir(schema).filter_map do |mod|
33
33
  path = File.join(protocol_dir, "#{mod.filename}.rb")
34
- File.exist?(path) && File.read(path) == render(mod, template)
34
+ "#{mod.filename}.rb" unless File.exist?(path) && File.read(path) == render(mod, template)
35
35
  end
36
+ stale << 'error_code.rb' unless error_module_current?(schema, protocol_dir)
36
37
  return if stale.empty?
37
38
 
38
- warn "Generated BiDi protocol code is stale or hand-edited: #{stale.map { |m| "#{m.filename}.rb" }.sort.join(', ')}"
39
+ warn "Generated BiDi protocol code is stale or hand-edited: #{stale.sort.join(', ')}"
39
40
  warn 'Regenerate with: bazel run //rb/lib/selenium/webdriver:bidi-generate'
40
41
  exit 1
41
42
  end
42
43
 
44
+ # Whether the checked-in protocol/error_code.rb matches what the generator would render now.
45
+ def self.error_module_current?(schema, protocol_dir)
46
+ mod = ErrorModule.new(filename: 'error_code', codes: error_code_map(schema))
47
+ path = File.join(protocol_dir, 'error_code.rb')
48
+ File.exist?(path) && File.read(path) == render(mod, File.join(__dir__, 'templates', 'error_code.rb.erb'))
49
+ end
50
+
43
51
  # $(rootpath) is relative to the runfiles root; __dir__ anchors us there so it resolves the
44
52
  # same way locally and on RBE (an execpath would not). This file lives at
45
53
  # rb/lib/selenium/webdriver/bidi/support, so expand six levels up to the root — File.expand_path
@@ -25,13 +25,18 @@ module Selenium
25
25
  #
26
26
  # @api private
27
27
  class Transport
28
+ # The websocket the transport sends over. Exposed so a domain can build a sibling
29
+ # domain (e.g. a vendor variant) over the same connection without Transport ever
30
+ # becoming a public constructor argument.
31
+ attr_reader :connection
32
+
28
33
  def initialize(connection)
29
34
  @connection = connection
30
35
  end
31
36
 
32
37
  def execute(cmd:, params: nil, result: nil)
33
38
  reply = @connection.send_cmd(method: cmd, params: serialize(params))
34
- raise Error::WebDriverError, error_message(reply) if reply['error']
39
+ raise error_for(reply) if reply['error']
35
40
 
36
41
  value = reply['result']
37
42
  result ? result.from_json(value) : value
@@ -43,8 +48,8 @@ module Selenium
43
48
  params&.as_json || {}
44
49
  end
45
50
 
46
- def error_message(reply)
47
- "#{reply['error']}: #{reply['message']}\n#{reply['stacktrace']}"
51
+ def error_for(reply)
52
+ Protocol::ErrorCode.for(reply['error']).new("#{reply['message']}\n#{reply['stacktrace']}")
48
53
  end
49
54
  end # Transport
50
55
  end # BiDi
@@ -27,8 +27,11 @@ module Selenium
27
27
  DRIVER_PATH_ENV_KEY = 'SE_CHROMEDRIVER'
28
28
 
29
29
  def initialize(args: nil, **)
30
+ args = Array(args.dup)
31
+ # skip when CHROME_LOG_FILE is set; --enable-chrome-logs would override the user's log file
32
+ args << '--enable-chrome-logs' unless ENV.key?('CHROME_LOG_FILE') || args.include?('--enable-chrome-logs')
33
+
30
34
  if ENV.key?('SE_DEBUG')
31
- args = Array(args.dup)
32
35
  warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
33
36
  args << '--verbose'
34
37
  end
@@ -27,7 +27,6 @@ module Selenium
27
27
 
28
28
  class Driver < WebDriver::Driver
29
29
  EXTENSIONS = [DriverExtensions::HasCDP,
30
- DriverExtensions::HasBiDi,
31
30
  DriverExtensions::HasCasting,
32
31
  DriverExtensions::HasFedCmDialog,
33
32
  DriverExtensions::HasNetworkConditions,
@@ -86,6 +86,7 @@ module Selenium
86
86
  @logging_prefs = options.delete(:logging_prefs) || {}
87
87
  @encoded_extensions = @options.delete(:encoded_extensions) || []
88
88
  @extensions = []
89
+ @vendor_options = {}
89
90
  @options.delete(:extensions).each { |ext| validate_extension(ext) }
90
91
  end
91
92
 
@@ -184,6 +185,24 @@ module Selenium
184
185
  @options[:emulation] = opts
185
186
  end
186
187
 
188
+ #
189
+ # Add a Chromium-specific capability nested in the browser options object
190
+ # (e.g. `goog:chromeOptions` / `ms:edgeOptions`) that is not yet exposed
191
+ # through a dedicated method. This is the escape hatch for legitimate
192
+ # vendor capabilities the bindings do not model.
193
+ #
194
+ # @example Set an option not handled by other methods
195
+ # options = Selenium::WebDriver::Chrome::Options.new
196
+ # options.add_chromium_option('unhandledCapability', 'value')
197
+ #
198
+ # @param [String, Symbol] name Name of the capability, as expected by the driver
199
+ # @param [Object] value Value of the capability
200
+ #
201
+
202
+ def add_chromium_option(name, value)
203
+ @vendor_options[name.to_s] = value
204
+ end
205
+
187
206
  #
188
207
  # Enables mobile browser use on Android.
189
208
  #
@@ -209,6 +228,7 @@ module Selenium
209
228
  enable_logging(browser_options) unless @logging_prefs.empty?
210
229
 
211
230
  options = browser_options[self.class::KEY]
231
+ options.merge!(@vendor_options)
212
232
  options['binary'] ||= binary_path if binary_path
213
233
 
214
234
  if @profile
@@ -90,6 +90,17 @@ module Selenium
90
90
  @bridge.status
91
91
  end
92
92
 
93
+ #
94
+ # Returns the WebDriver BiDi connection for this session.
95
+ #
96
+ # @return [BiDi]
97
+ # @raise [Error::WebDriverError] if BiDi was not enabled for this session
98
+ #
99
+
100
+ def bidi
101
+ @bridge.bidi
102
+ end
103
+
93
104
  #
94
105
  # @return [Navigation]
95
106
  # @see Navigation
@@ -57,13 +57,6 @@ module Selenium
57
57
  #
58
58
 
59
59
  def on_log_event(kind, &block)
60
- if browser == :firefox
61
- WebDriver.logger.deprecate(
62
- 'Driver#on_log_event on Firefox',
63
- 'the script.add_console_message_handler or the script.add_javascript_error_handler methods',
64
- id: :on_log_event
65
- )
66
- end
67
60
  raise Error::WebDriverError, "Don't know how to handle #{kind} events" unless KINDS.include?(kind)
68
61
 
69
62
  enabled = log_listeners[kind].any?
@@ -60,13 +60,6 @@ module Selenium
60
60
  #
61
61
 
62
62
  def intercept(&block)
63
- if browser == :firefox
64
- WebDriver.logger.deprecate(
65
- 'Driver#intercept on Firefox',
66
- 'the new bidi.network.add_intercept method',
67
- id: :intercept
68
- )
69
- end
70
63
  @interceptor ||= DevTools::NetworkInterceptor.new(devtools)
71
64
  @interceptor.intercept(&block)
72
65
  end
@@ -52,8 +52,8 @@ module Selenium
52
52
  path = @service.executable_path || env_path || class_path
53
53
  path ? paths_from_service(path) : paths_from_manager
54
54
  rescue StandardError => e
55
- WebDriver.logger.error("Exception occurred: #{e.message}")
56
- WebDriver.logger.error("Backtrace:\n\t#{e.backtrace&.join("\n\t")}")
55
+ WebDriver.logger.error("Exception occurred: #{e.message}", id: :driver_finder)
56
+ WebDriver.logger.error("Backtrace:\n\t#{e.backtrace&.join("\n\t")}", id: :driver_finder)
57
57
  raise Error::NoSuchDriverError, "Unable to obtain #{@service.class::EXECUTABLE}"
58
58
  end
59
59
  end
@@ -69,7 +69,8 @@ module Selenium
69
69
 
70
70
  def paths_from_service(path)
71
71
  exe = @service.class::EXECUTABLE
72
- WebDriver.logger.debug("Skipping Selenium Manager; path to #{exe} specified in service class: #{path}")
72
+ WebDriver.logger.debug("Skipping Selenium Manager; path to #{exe} specified in service class: #{path}",
73
+ id: :driver_finder)
73
74
  Platform.assert_executable(path)
74
75
  {driver_path: path}
75
76
  end
@@ -75,6 +75,8 @@ module Selenium
75
75
 
76
76
  @options = opts
77
77
  @options[:browser_name] = self.class::BROWSER
78
+
79
+ enable_bidi! if @options[:web_socket_url]
78
80
  end
79
81
 
80
82
  #
@@ -93,6 +95,12 @@ module Selenium
93
95
  @options[name] = value
94
96
  end
95
97
 
98
+ #
99
+ # Enables WebDriver BiDi by requesting the W3C webSocketUrl capability.
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+
96
104
  def enable_bidi!
97
105
  @options[:web_socket_url] = true
98
106
  end
@@ -130,11 +138,23 @@ module Selenium
130
138
  browser_options = {self.class::KEY => browser_options} if defined?(self.class::KEY)
131
139
 
132
140
  process_browser_options(browser_options)
133
- generate_as_json(w3c_options.merge(browser_options))
141
+ generate_as_json(merge_browser_options(w3c_options, browser_options))
134
142
  end
135
143
 
136
144
  private
137
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
+
138
158
  def w3c?(key)
139
159
  W3C_OPTIONS.include?(key) || key.to_s.include?(':')
140
160
  end
@@ -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,7 +136,6 @@ 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
140
  'noProxy' => no_proxy.is_a?(String) ? no_proxy.split(',').map(&:strip).reject(&:empty?) : no_proxy,
149
141
  'proxyAutoconfigUrl' => pac,
@@ -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
 
@@ -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
 
@@ -21,6 +21,7 @@ require 'websocket'
21
21
 
22
22
  module Selenium
23
23
  module WebDriver
24
+ # @api private
24
25
  class WebSocketConnection
25
26
  CONNECTION_ERRORS = [
26
27
  Errno::ECONNRESET, # connection is aborted (browser process was killed)
@@ -35,6 +36,15 @@ module Selenium
35
36
 
36
37
  MAX_LOG_MESSAGE_SIZE = 9999
37
38
 
39
+ # websocket-ruby defaults to a 20MB limit and silently drops larger
40
+ # frames, which can stall the listener. CDP payloads (e.g. large data:
41
+ # URLs) can exceed that, so raise the ceiling for our connections.
42
+ # The gem only exposes the limit as process-global state, so it is
43
+ # raised (never lowered) and not restored on close - concurrent
44
+ # connections share the value. Other bindings rely on their own
45
+ # websocket clients' limits; this constant only affects websocket-ruby.
46
+ MAX_FRAME_SIZE = 100 * 1024 * 1024 # 100MB
47
+
38
48
  def initialize(url:)
39
49
  @callback_threads = ThreadGroup.new
40
50
 
@@ -46,26 +56,19 @@ module Selenium
46
56
  @session_id = nil
47
57
  @url = url
48
58
 
59
+ apply_frame_size_limit
49
60
  process_handshake
50
61
  @socket_thread = attach_socket_listener
51
62
  end
52
63
 
64
+ # Idempotent: the listener may already have initiated shutdown (see
65
+ # #frame_dropped?), so always close the socket and join threads rather
66
+ # than short-circuiting on @closing.
53
67
  def 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
68
+ close_socket
66
69
 
67
70
  # Let threads unwind instead of calling exit
68
- @socket_thread&.join(0.5)
71
+ @socket_thread&.join(0.5) unless @socket_thread == Thread.current
69
72
  @callback_threads.list.each do |thread|
70
73
  thread.join(0.5)
71
74
  rescue StandardError => e
@@ -97,6 +100,9 @@ module Selenium
97
100
  end
98
101
 
99
102
  def send_cmd(**payload)
103
+ # IOError to match what writing to an already-closed socket raises
104
+ raise IOError, 'WebSocket connection is closed' if @closing
105
+
100
106
  id = next_id
101
107
  data = payload.merge(id: id)
102
108
  WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
@@ -109,7 +115,11 @@ module Selenium
109
115
  raise e, "WebSocket is closed (#{e.class}: #{e.message})"
110
116
  end
111
117
 
112
- wait.until { @messages_mtx.synchronize { messages.delete(id) } }
118
+ wait.until do
119
+ raise IOError, 'WebSocket connection closed while waiting for a response' if @closing
120
+
121
+ @messages_mtx.synchronize { messages.delete(id) }
122
+ end
113
123
  end
114
124
 
115
125
  private
@@ -132,22 +142,51 @@ module Selenium
132
142
 
133
143
  incoming_frame << socket.readpartial(1024)
134
144
 
135
- while (frame = incoming_frame.next)
136
- break if @closing
137
-
138
- message = process_frame(frame)
139
- next unless message['method']
140
-
141
- @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
142
- @callback_threads.add(callback_thread(message['params'], &callback))
143
- end
144
- end
145
+ process_incoming_frames
146
+ break if frame_dropped?
145
147
  end
146
148
  rescue *CONNECTION_ERRORS, WebSocket::Error => e
147
149
  WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
148
150
  end
149
151
  end
150
152
 
153
+ def close_socket
154
+ @closing_mtx.synchronize { @closing = true }
155
+ socket.close
156
+ rescue *CONNECTION_ERRORS => e
157
+ WebDriver.logger.debug "WebSocket socket closed: #{e.class}: #{e.message}", id: :ws
158
+ end
159
+
160
+ def process_incoming_frames
161
+ while (frame = incoming_frame.next)
162
+ break if @closing
163
+
164
+ message = process_frame(frame)
165
+ next unless message['method']
166
+
167
+ @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
168
+ @callback_threads.add(callback_thread(message['params'], &callback))
169
+ end
170
+ end
171
+ end
172
+
173
+ # True when the buffered frame could not be decoded (e.g. exceeds MAX_FRAME_SIZE).
174
+ # websocket-ruby swallows the error and keeps returning nil, so surface it and close
175
+ # the connection here instead of leaving a dead listener on an open socket.
176
+ def frame_dropped?
177
+ return false unless incoming_frame.error?
178
+
179
+ WebDriver.logger.error("WebSocket frame dropped (#{incoming_frame.error}); if payloads can legitimately " \
180
+ "exceed #{WebSocket.max_frame_size} bytes, set WebSocket.max_frame_size " \
181
+ 'to a higher value', id: :ws)
182
+ close_socket
183
+ true
184
+ end
185
+
186
+ def apply_frame_size_limit
187
+ WebSocket.max_frame_size = MAX_FRAME_SIZE if WebSocket.max_frame_size < MAX_FRAME_SIZE
188
+ end
189
+
151
190
  def incoming_frame
152
191
  @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
153
192
  end
@@ -204,8 +243,7 @@ module Selenium
204
243
  end
205
244
 
206
245
  def next_id
207
- @id ||= 0
208
- @id += 1
246
+ @id = (@id || 0) + 1
209
247
  end
210
248
  end # BiDi
211
249
  end # WebDriver
@@ -76,7 +76,6 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
76
76
  require 'selenium/webdriver/common/driver_extensions/uploads_files'
77
77
  require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
78
78
  require 'selenium/webdriver/common/driver_extensions/has_addons'
79
- require 'selenium/webdriver/common/driver_extensions/has_bidi'
80
79
  require 'selenium/webdriver/common/driver_extensions/has_devtools'
81
80
  require 'selenium/webdriver/common/driver_extensions/has_file_downloads'
82
81
  require 'selenium/webdriver/common/driver_extensions/has_session_events'
@@ -27,8 +27,11 @@ module Selenium
27
27
  DRIVER_PATH_ENV_KEY = 'SE_EDGEDRIVER'
28
28
 
29
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
+
30
34
  if ENV.key?('SE_DEBUG')
31
- args = Array(args.dup)
32
35
  warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
33
36
  args << '--verbose'
34
37
  end
@@ -29,9 +29,6 @@ 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
@@ -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
@@ -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
@@ -18,40 +18,55 @@
18
18
  # under the License.
19
19
 
20
20
  require 'selenium/webdriver/bidi'
21
- require 'selenium/webdriver/bidi/transport'
21
+ require 'selenium/webdriver/bidi/protocol'
22
22
 
23
23
  module Selenium
24
24
  module WebDriver
25
25
  module Remote
26
26
  class BiDiBridge < Bridge
27
- attr_reader :bidi, :transport
27
+ attr_reader :bidi, :connection
28
+
29
+ READINESS_STATE = {
30
+ 'none' => :none,
31
+ 'eager' => :interactive,
32
+ 'normal' => :complete
33
+ }.freeze
28
34
 
29
35
  def create_session(capabilities)
30
36
  super
31
- socket_url = @capabilities[:web_socket_url]
32
- @bidi = Selenium::WebDriver::BiDi.new(url: socket_url)
33
- # Share the BiDi object's socket until the bridge owns the connection directly.
34
- @transport = BiDi::Transport.new(@bidi.ws)
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
35
46
  end
36
47
 
37
48
  def get(url)
38
- browsing_context.navigate(url)
49
+ browsing_context.navigate(context: window_handle, url: url, wait: readiness_state)
50
+ nil
39
51
  end
40
52
 
41
53
  def go_back
42
- browsing_context.traverse_history(-1)
54
+ browsing_context.traverse_history(context: window_handle, delta: -1)
55
+ nil
43
56
  end
44
57
 
45
58
  def go_forward
46
- browsing_context.traverse_history(1)
59
+ browsing_context.traverse_history(context: window_handle, delta: 1)
60
+ nil
47
61
  end
48
62
 
49
63
  def refresh
50
- browsing_context.reload
64
+ browsing_context.reload(context: window_handle, wait: readiness_state)
65
+ nil
51
66
  end
52
67
 
53
68
  def quit
54
- bidi.close
69
+ bidi&.close
55
70
  rescue *QUIT_ERRORS
56
71
  nil
57
72
  ensure
@@ -64,8 +79,20 @@ module Selenium
64
79
 
65
80
  private
66
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
+
67
90
  def browsing_context
68
- @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')
69
96
  end
70
97
  end # BiDiBridge
71
98
  end # Remote
@@ -598,7 +598,7 @@ module Selenium
598
598
  raise(WebDriver::Error::WebDriverError, msg)
599
599
  end
600
600
 
601
- def transport
601
+ def connection
602
602
  msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
603
603
  raise(WebDriver::Error::WebDriverError, msg)
604
604
  end