selenium-webdriver 4.46.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +29 -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 +5971 -0
  6. data/bin/selenium-manager.cdx.json +11629 -0
  7. data/bin/windows/selenium-manager.exe +0 -0
  8. data/lib/selenium/webdriver/{common/driver_extensions/has_bidi.rb → bidi/error.rb} +21 -13
  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 +194 -18
  27. data/lib/selenium/webdriver/bidi/serialization/union.rb +45 -3
  28. data/lib/selenium/webdriver/bidi/serialization.rb +13 -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.rb +0 -1
  44. data/lib/selenium/webdriver/edge/service.rb +4 -1
  45. data/lib/selenium/webdriver/firefox/driver.rb +0 -3
  46. data/lib/selenium/webdriver/firefox/options.rb +19 -0
  47. data/lib/selenium/webdriver/firefox/profile.rb +13 -6
  48. data/lib/selenium/webdriver/remote/bidi_bridge.rb +39 -12
  49. data/lib/selenium/webdriver/remote/bridge.rb +1 -1
  50. data/lib/selenium/webdriver/remote/driver.rb +3 -1
  51. data/lib/selenium/webdriver/remote/http/default.rb +1 -1
  52. data/lib/selenium/webdriver/safari/options.rb +16 -1
  53. data/lib/selenium/webdriver/support/guards/guard.rb +20 -3
  54. data/lib/selenium/webdriver/support/guards.rb +9 -3
  55. data/lib/selenium/webdriver/version.rb +1 -1
  56. metadata +6 -3
@@ -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
 
@@ -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
@@ -37,7 +37,9 @@ module Selenium
37
37
 
38
38
  caps = process_options(options, capabilities)
39
39
  http_client ||= Remote::Http::Default.new(client_config: client_config)
40
- http_client.server_url = url || client_config&.server_url || "http://#{Platform.localhost}:4444/wd/hub"
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('/')
41
43
  super(caps: caps, http_client: http_client, **)
42
44
  @bridge.file_detector = ->((filename, *)) { File.exist?(filename) && filename.to_s }
43
45
  command_list = @bridge.command_list
@@ -124,7 +124,7 @@ module Selenium
124
124
  end
125
125
 
126
126
  def follow_redirect(response, redirects)
127
- WebDriver.logger.debug("Redirect to #{response['Location']}; times: #{redirects}")
127
+ WebDriver.logger.debug("Redirect to #{response['Location']}; times: #{redirects}", id: :redirect)
128
128
  raise Error::WebDriverError, 'too many redirects' if redirects >= client_config.max_redirects
129
129
 
130
130
  request(:get, URI.parse(response['Location']), DEFAULT_HEADERS.dup, nil, redirects + 1)
@@ -25,7 +25,8 @@ module Selenium
25
25
 
26
26
  # @see https://developer.apple.com/documentation/webkit/about_webdriver_for_safari
27
27
  CAPABILITIES = {automatic_inspection: 'safari:automaticInspection',
28
- automatic_profiling: 'safari:automaticProfiling'}.freeze
28
+ automatic_profiling: 'safari:automaticProfiling',
29
+ experimental_web_socket_url: 'safari:experimentalWebSocketUrl'}.freeze
29
30
  BROWSER = 'safari'
30
31
  TECHNOLOGY_PREVIEW = 'Safari Technology Preview'
31
32
 
@@ -43,6 +44,20 @@ module Selenium
43
44
  def browser_name
44
45
  @options[:browser_name] = Safari.technology_preview? ? TECHNOLOGY_PREVIEW : BROWSER
45
46
  end
47
+
48
+ #
49
+ # Enables WebDriver BiDi for Safari, which also requires the experimental capability, and
50
+ # warns that Safari's BiDi support is experimental.
51
+ #
52
+ # @return [Boolean]
53
+ #
54
+
55
+ def enable_bidi!
56
+ super
57
+ WebDriver.logger.warn("Safari's WebDriver BiDi support is experimental and may be incomplete",
58
+ id: :safari_bidi)
59
+ @options[:experimental_web_socket_url] = true
60
+ end
46
61
  end # Options
47
62
  end # Safari
48
63
  end # WebDriver
@@ -30,7 +30,7 @@ module Selenium
30
30
  attr_reader :guarded, :type, :messages, :reason, :tracker
31
31
 
32
32
  def initialize(guarded, type, guards = nil)
33
- @guarded = guarded
33
+ @guarded = guarded.dup
34
34
  @tracker = guards&.bug_tracker || ''
35
35
  @messages = guards&.messages || {}
36
36
  @messages[:unknown] = 'TODO: Investigate why this is failing and file a bug report'
@@ -47,7 +47,7 @@ module Selenium
47
47
  when Symbol
48
48
  messages[reason]
49
49
  else
50
- "Guarded by #{guarded};"
50
+ "#{type.to_s.tr('_', ' ')} #{guarded};"
51
51
  end
52
52
 
53
53
  case type
@@ -57,8 +57,10 @@ module Selenium
57
57
  "Test skipped because it is unreliable in this configuration; #{details}"
58
58
  when :skip_unless, :exclusive
59
59
  "Test does not apply to this configuration; #{details}"
60
- else
60
+ when :pending_if, :pending_unless, :except, :only
61
61
  "Test guarded; #{details}"
62
+ else
63
+ raise ArgumentError, "unknown guard type: #{type}"
62
64
  end
63
65
  end
64
66
 
@@ -81,6 +83,21 @@ module Selenium
81
83
  def exclusive?
82
84
  @type == :skip_unless || @type == :exclusive
83
85
  end
86
+
87
+ # A pending guard that only applies when the failure matches an expected exception.
88
+ def exception?
89
+ (except? || only?) && !@guarded[:exception].nil?
90
+ end
91
+
92
+ # Whether the exception is the guard's `exception:` class and matches its optional `message:`
93
+ # (Regexp pattern or exact String), following RSpec's `raise_error` semantics.
94
+ def matches_exception?(exception)
95
+ spec = @guarded[:exception]
96
+ return false unless spec && exception.is_a?(spec[:class])
97
+
98
+ message = spec[:message]
99
+ message.nil? || (message.is_a?(Regexp) ? message.match?(exception.message) : message == exception.message)
100
+ end
84
101
  end # Guard
85
102
  end # Guards
86
103
  end # Support
@@ -41,7 +41,7 @@ module Selenium
41
41
  def add_condition(name, condition = false, &block)
42
42
  condition = false if condition.nil?
43
43
  @guard_conditions << GuardCondition.new(name, condition, &block)
44
- WebDriver.logger.debug "Running with Guard '#{name}' set to: #{condition}"
44
+ WebDriver.logger.debug "Running with Guard '#{name}' set to: #{condition}", id: :guard
45
45
  end
46
46
 
47
47
  def add_message(name, message)
@@ -52,12 +52,18 @@ module Selenium
52
52
  if !skipping_guard.nil?
53
53
  [:skip, skipping_guard.message]
54
54
  elsif !pending_guard.nil? && ENV.fetch('SKIP_PENDING', nil)
55
- [:skip, pending_guard.message]
56
- elsif !pending_guard.nil?
55
+ [:skip, "(skipped by SKIP_PENDING) #{pending_guard.message}"]
56
+ elsif !pending_guard.nil? && !pending_guard.exception?
57
57
  [:pending, pending_guard.message]
58
58
  end
59
59
  end
60
60
 
61
+ # The deferred `exception:` pending guard, evaluated against the failure after the run, else nil.
62
+ def pending_exception_guard
63
+ guard = pending_guard
64
+ guard if disposition.nil? && guard&.exception?
65
+ end
66
+
61
67
  def satisfied?(guard)
62
68
  @guard_conditions.all? { |condition| condition.satisfied?(guard) }
63
69
  end