Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions rb/lib/selenium/webdriver/chromium/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ module Features
get_log: [:post, 'session/:session_id/se/log']
}.freeze

# TODO: remove subclass override once chrome supports base64; currently only supports an unpacked local path
def install_web_extension(path)
result = web_extension.install(extension_data: web_extension.extension_path(path: path))
WebExtension.new(result.extension)
end

def launch_app(id)
execute :launch_app, {}, {id: id}
end
Expand Down
4 changes: 4 additions & 0 deletions rb/lib/selenium/webdriver/chromium/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ def process_browser_options(browser_options)
options['args'] << "--user-data-dir=#{@profile.directory}"
end

if bidi?
options['args'] = options['args'].to_a | %w[--enable-unsafe-extension-debugging --remote-debugging-pipe]
end
Comment on lines +239 to +241

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Bidi args nomethoderror 🐞 Bug ☼ Reliability

In Chromium::Options#process_browser_options, when BiDi is enabled it calls options['args'].to_a,
which raises NoMethodError if args is a String (or other non-Array) that can be set via Options
initialization/merge or add_option. This crashes option processing before session start, instead
of producing a clear argument/type error.
Agent Prompt
### Issue description
When BiDi is enabled, Chromium options processing does `options['args'].to_a`, which can raise `NoMethodError` if `args` is not an Array (e.g., a String). This prevents session creation and yields an opaque error.

### Issue Context
`Chromium::Options#initialize` merges defaults with user-supplied `@options`, so `args:` provided as a non-Array can override the default `[]`. `Common::Options#add_option` also stores values without type validation.

### Fix Focus Areas
- rb/lib/selenium/webdriver/chromium/options.rb[227-241]

### Suggested change
- Replace `options['args'].to_a` with safer normalization, e.g.:
  - `args = options['args']
    args = args.nil? ? [] : Array(args)
    options['args'] = args | %w[--enable-unsafe-extension-debugging --remote-debugging-pipe]`
- Alternatively, if you want strictness, explicitly raise a `WebDriverError` when `options['args']` is present and not an `Array`, with a clear message (`'args' must be an Array of Strings`).

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return if (@encoded_extensions + @extensions).empty?

options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) }
Expand Down
1 change: 1 addition & 0 deletions rb/lib/selenium/webdriver/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
require 'selenium/webdriver/common/takes_screenshot'
require 'selenium/webdriver/common/driver'
require 'selenium/webdriver/common/element'
require 'selenium/webdriver/common/web_extension'
require 'selenium/webdriver/common/shadow_root'
require 'selenium/webdriver/common/websocket_connection'
require 'selenium/webdriver/common/child_process'
Expand Down
28 changes: 28 additions & 0 deletions rb/lib/selenium/webdriver/common/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,34 @@ def network
@network ||= WebDriver::Network.new(bridge)
end

#
# Installs a browser extension over WebDriver BiDi (+webExtension.install+).
#
# Firefox sends the extension base64-encoded, so it accepts an unpacked directory, a packed
# extension (.xpi/.crx/.zip), or already-encoded base64 bytes, and works with remote (Grid)
# sessions. Chromium browsers currently accept only an unpacked directory whose path resolves on
# the browser host (local sessions), until chromium-bidi supports base64 (SeleniumHQ/selenium#16541).
#
# @note Requires a BiDi session (set +web_socket_url+ to true in the browser options).
# @param [String] path unpacked extension directory, packed extension file, or base64-encoded bytes
# @return [WebExtension] handle for the installed extension
#

def install_web_extension(...)
bridge.install_web_extension(...)
end

#
# Uninstalls a browser extension installed with {#install_web_extension}.
#
# @note Requires a BiDi session (set +web_socket_url+ to true in the browser options).
# @param [WebExtension] extension handle returned by {#install_web_extension}
#

def uninstall_web_extension(extension)
bridge.uninstall_web_extension(extension.id)
end

#-------------------------------- sugar --------------------------------

#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module HasAddons
#

def install_addon(path, temporary = nil)
WebDriver.logger.deprecate('#install_addon', '#install_web_extension', id: :install_addon)
@bridge.install_addon(path, temporary)
end

Expand All @@ -40,6 +41,7 @@ def install_addon(path, temporary = nil)
#

def uninstall_addon(id)
WebDriver.logger.deprecate('#uninstall_addon', '#uninstall_web_extension', id: :uninstall_addon)
@bridge.uninstall_addon(id)
end
end # HasAddons
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ module HasDevTools
# Retrieves connection to DevTools.
#
# @return [DevTools]
# @raise [Error::WebDriverError] when BiDi is enabled, as CDP shares a transport with it
#

def devtools(target_type: 'page')
if @bridge.bidi?
raise Error::WebDriverError,
'CDP (DevTools) is disabled when BiDi is enabled; use the WebDriver BiDi APIs instead'
end
Comment on lines +32 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. devtools raises under bidi πŸ“˜ Rule violation βš™ Maintainability

Calling driver.devtools now raises when @bridge.bidi? is true, which is a user-visible behavior
change for BiDi users without an explicit deprecation/migration period. This can break downstream
code that previously used CDP while experimenting with BiDi.
Agent Prompt
## Issue description
`Driver#devtools` now raises when BiDi is enabled, which is a breaking user-visible behavior change without an explicit deprecation/migration period.

## Issue Context
The PR introduces a hard failure path (`raise Error::WebDriverError`) for `devtools` when `@bridge.bidi?` is true. To maintain compatibility expectations, provide an explicit deprecation/migration path (or a documented compatibility switch) before enforcing this behavior.

## Fix Focus Areas
- rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb[28-35]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


@devtools ||= {}
@devtools[target_type] ||= begin
require 'selenium/devtools'
Expand Down
42 changes: 42 additions & 0 deletions rb/lib/selenium/webdriver/common/web_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
module WebDriver
#
# Handle for a browser extension installed via Driver#install_web_extension.
# Wraps the identifier the browser assigned; pass it to Driver#uninstall_web_extension.
#
class WebExtension
#
# @return [String] identifier assigned to the extension by the browser
#

attr_reader :id

#
# @api private
#

def initialize(id)
@id = id
end
end # WebExtension
end # WebDriver
end # Selenium
29 changes: 29 additions & 0 deletions rb/lib/selenium/webdriver/firefox/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ def uninstall_addon(id)
execute :uninstall_addon, {}, {id: id}
end

def install_web_extension(path, allow_private_browsing: nil, permanent: nil)
data = encode_extension(path)
return classic_install_web_extension(data, permanent, allow_private_browsing) unless bidi?

moz = web_extension.moz
options = {allow_private_browsing:, permanent:}.compact
result = moz.install(extension_data: moz.extension_base64_encoded(value: data), **options)
WebDriver::WebExtension.new(result.extension)
end

def uninstall_web_extension(extension_id)
bidi? ? web_extension.uninstall(extension: extension_id) : uninstall_addon(extension_id)
nil
end

def full_screenshot
execute :full_page_screenshot
end
Expand All @@ -64,6 +79,20 @@ def context=(context)
def context
execute :get_context
end

private

def classic_install_web_extension(data, permanent, allow_private_browsing)
if allow_private_browsing == false
raise Error::WebDriverError,
'allow_private_browsing: false requires a BiDi session; the classic install always grants ' \
'private-browsing access'
end

temporary = !permanent unless permanent.nil?
options = {temporary: temporary, allowPrivateBrowsing: allow_private_browsing}.compact
WebDriver::WebExtension.new(execute(:install_addon, {}, {addon: data, **options}))
end
end # Bridge
end # Firefox
end # WebDriver
Expand Down
15 changes: 15 additions & 0 deletions rb/lib/selenium/webdriver/remote/bidi_bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def create_session(capabilities)
end
end

def install_web_extension(path)
data = web_extension.extension_base64_encoded(value: encode_extension(path))
result = web_extension.install(extension_data: data)
WebExtension.new(result.extension)
end

def uninstall_web_extension(id)
web_extension.uninstall(extension: id)
nil
end

def get(url)
browsing_context.navigate(context: window_handle, url: url, wait: readiness_state)
nil
Expand Down Expand Up @@ -91,6 +102,10 @@ def browsing_context
@browsing_context ||= BiDi::Protocol::BrowsingContext.new(connection)
end

def web_extension
@web_extension ||= BiDi::Protocol::WebExtension.new(connection)
end

def readiness_state
READINESS_STATE.fetch(capabilities[:page_load_strategy] || 'normal')
end
Expand Down
26 changes: 20 additions & 6 deletions rb/lib/selenium/webdriver/remote/bridge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -593,14 +593,18 @@ def click_fedcm_dialog_button
execute :click_fedcm_dialog_button, {}, {dialogButton: 'ConfirmIdpLoginContinue'}
end

def bidi
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
raise(WebDriver::Error::WebDriverError, msg)
def bidi(*)
raise WebDriver::Error::WebDriverError,
'BiDi must be enabled by setting #web_socket_url to true in options class'
end
alias connection bidi
alias web_extension bidi
alias install_web_extension bidi
alias uninstall_web_extension bidi
private :web_extension

def connection
msg = 'BiDi must be enabled by setting #web_socket_url to true in options class'
raise(WebDriver::Error::WebDriverError, msg)
def bidi?
!@bidi.nil?
end

def command_list
Expand All @@ -609,6 +613,16 @@ def command_list

private

def encode_extension(path)
if File.directory?(path)
Zipper.zip(path)
elsif File.file?(path)
File.open(path, 'rb') { |file| Base64.strict_encode64(file.read) }
else
path # already base64-encoded bytes
end
end

#
# executes a command on the remote server.
#
Expand Down
6 changes: 6 additions & 0 deletions rb/sig/interfaces/bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@

interface _Bridge
def execute: (untyped command, ?Hash[untyped, untyped] opts, ?untyped? command_hash) -> untyped

def bidi?: () -> bool

def web_extension: () -> Selenium::WebDriver::BiDi::Protocol::WebExtension

def encode_extension: (String path) -> String
end
2 changes: 2 additions & 0 deletions rb/sig/lib/selenium/webdriver/chromium/features.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ module Selenium

def commands: (Symbol command) -> Array[Symbol | String]

def install_web_extension: (String path) -> Selenium::WebDriver::WebExtension

def launch_app: (String id) -> String

def cast_sinks: () -> Array[String]
Expand Down
4 changes: 4 additions & 0 deletions rb/sig/lib/selenium/webdriver/common/driver.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ module Selenium

def add_virtual_authenticator: (untyped options) -> VirtualAuthenticator

def install_web_extension: (String path, **untyped options) -> WebExtension

def uninstall_web_extension: (WebExtension extension) -> void

alias first find_element

alias all find_elements
Expand Down
29 changes: 29 additions & 0 deletions rb/sig/lib/selenium/webdriver/common/web_extension.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.


module Selenium
module WebDriver
class WebExtension
@id: String

attr_reader id: String

def initialize: (String id) -> void
end
end
end
8 changes: 8 additions & 0 deletions rb/sig/lib/selenium/webdriver/firefox/features.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ module Selenium

def uninstall_addon: (untyped id) -> untyped

def install_web_extension: (String path, ?allow_private_browsing: bool?, ?permanent: bool?) -> Selenium::WebDriver::WebExtension

def uninstall_web_extension: (String extension_id) -> void

def full_screenshot: () -> untyped

def context=: (untyped context) -> untyped

def context: () -> untyped

private

def classic_install_web_extension: (String data, bool? permanent, bool? allow_private_browsing) -> Selenium::WebDriver::WebExtension
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ module Selenium

@connection: untyped

@web_extension: BiDi::Protocol::WebExtension

attr_reader bidi: BiDi

attr_reader connection: untyped

def create_session: (untyped capabilities) -> void

def install_web_extension: (String path) -> WebExtension

def uninstall_web_extension: (String id) -> void

def get: (String url) -> void

def go_back: () -> void
Expand All @@ -50,6 +56,8 @@ module Selenium

def browsing_context: () -> BiDi::Protocol::BrowsingContext

def web_extension: () -> BiDi::Protocol::WebExtension

def readiness_state: () -> Symbol
end
end
Expand Down
10 changes: 10 additions & 0 deletions rb/sig/lib/selenium/webdriver/remote/bridge.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,14 @@ module Selenium

def bidi: -> BiDi

def bidi?: () -> bool

def connection: -> untyped

def install_web_extension: (String path) -> WebExtension

def uninstall_web_extension: (String id) -> void

def cancel_fedcm_dialog: -> nil

def click_fedcm_dialog_button: -> nil
Expand Down Expand Up @@ -252,6 +258,10 @@ module Selenium

private

def web_extension: () -> WebDriver::BiDi::Protocol::WebExtension

def encode_extension: (String path) -> String

def execute: (untyped command, ?::Hash[untyped, untyped] opts, ?untyped? command_hash) -> String

def escaper: () -> untyped
Expand Down
Loading
Loading