Skip to content

[rb] Add cross-browser Driver#install_web_extension - #17879

Draft
titusfortner wants to merge 1 commit into
SeleniumHQ:trunkfrom
titusfortner:rb-install-web-extension
Draft

[rb] Add cross-browser Driver#install_web_extension#17879
titusfortner wants to merge 1 commit into
SeleniumHQ:trunkfrom
titusfortner:rb-install-web-extension

Conversation

@titusfortner

Copy link
Copy Markdown
Member

🔗 Related Issues

Prototype implementation for proposed ADR: #17817
This PR will remain in Draft until that proposal is approved

💥 What does this PR do?

Implements the ADR decisions:

  1. Adds cross-browser Driver#install_web_extension and Driver#uninstall_web_extension, backed by the
    generated BiDi webExtension protocol classes. Install accepts a directory, packed archive, or base64 bytes
    (plus Firefox's permanent / allow_private_browsing options) and returns a WebExtension handle wrapping
    the id; uninstall takes that handle.
  2. Backwards compatible: on Firefox the methods also work with BiDi off (classic moz/addon endpoint), and
    the legacy install_addon / uninstall_addon are deprecated toward the new methods — no routing.
  3. Enabling BiDi disables the CDP API: Chromium auto-passes --remote-debugging-pipe and
    --enable-unsafe-extension-debugging (no user flags), and Selenium's CDP API raises while BiDi is on.
  • Firefox transmits the extension base64-encoded, so all three inputs and remote/Grid sessions work,
    over BiDi or the classic endpoint. install_addon / uninstall_addon now emit a deprecation warning.
  • Chromium installs an unpacked local directory only (see Chromium note below); enabling BiDi switches it
    to the pipe transport and allows unsigned extensions with no user flags.
  • Without a BiDi session, a browser with no classic install path (Chromium, Safari, …) raises a clear
    "enable BiDi" error rather than a NoMethodError. On Firefox, allow_private_browsing: false also raises,
    since the classic endpoint always grants private-browsing access and cannot disable it.

🔧 Implementation Notes

  • Encoding (encode_extension) lives on the base bridge: it zips a directory, base64-encodes a file, or
    passes base64 through. Both the base BiDi path and Firefox::Features use it; Firefox::Features branches
    on bidi? — BiDi uses the moz vendor variant, classic uses the moz/addon endpoint.
  • Chromium can't meet the ADR as written yet. chromium-bidi implements neither base64 nor archivePath,
    so Chromium can only install an unpacked directory whose path resolves on the browser host (local sessions)
    — no packed archive, no Grid. A temporary Chromium::Features override sends that directory path directly;
    it is removed once chromium-bidi supports base64 ([🐛 Bug]: [python][Chrome] Webextension - no support for archived and base64 extensionData type in Chrome? #16541), at which point Chromium picks up archive/base64/Grid
    support with no API change.
  • The BiDi-gated bridge methods (bidi, connection, web_extension, install_web_extension,
    uninstall_web_extension) share a single "enable BiDi" raiser via aliases, so a browser with no classic
    path gets the same error everywhere.

🤖 AI assistance

  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: implementation, RBS signatures, and specs
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

🔄 Types of changes

  • New feature (non-breaking change which adds functionality and tests!)

@selenium-ci selenium-ci added C-rb Ruby Bindings B-build Includes scripting, bazel and CI integrations labels Aug 5, 2026
@titusfortner
titusfortner marked this pull request as ready for review August 5, 2026 23:26
@titusfortner
titusfortner marked this pull request as draft August 5, 2026 23:26
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add cross-browser Driver#install_web_extension via BiDi with Firefox fallback

✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add BiDi-based install/uninstall web extension APIs returning a WebExtension handle.
• Keep Firefox compatibility without BiDi via classic moz/addon endpoint and deprecate
 install_addon.
• Disable CDP DevTools when BiDi is enabled and add Chromium flags required for extension debugging.
Diagram

graph TD
  A["User test/code"] --> B["Driver API"] --> C["Remote::Bridge"] --> D{ "BiDi enabled?" }
  D -->|Yes| E["Remote::BiDiBridge"] --> F["BiDi WebExtension"]
  D -->|No (Firefox)| G["Firefox classic addon"]
  B --> H["WebExtension handle"]
  subgraph Legend
    direction LR
    _api(["Public API"]) ~~~ _mod["Module/bridge"] ~~~ _dec{"Decision"} ~~~ _ext[["External/Protocol"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep CDP available via separate transport/session
  • ➕ Avoids breaking existing CDP tooling when users enable BiDi
  • ➕ Enables mixed BiDi + CDP workflows (common in Chromium automation)
  • ➖ Higher implementation complexity; requires transport multiplexing and clear lifecycle rules
  • ➖ Risk of subtle race conditions if two protocols share browser resources
2. Make install_web_extension BiDi-only (no Firefox classic fallback)
  • ➕ Simplifies implementation and reduces branching/testing matrix
  • ➕ Encourages consistent cross-browser behavior aligned with the ADR direction
  • ➖ Breaks existing Firefox workflows where BiDi is not yet adopted
  • ➖ Reduces usability for users who can’t enable BiDi in their environment
3. Centralize Chromium/Firebase-specific install behavior behind capability negotiation
  • ➕ Avoids subclass overrides (easier long-term maintenance)
  • ➕ Can automatically pick best supported install mode (base64 vs path)
  • ➖ Requires upstream capability surfacing (chromium-bidi) that isn’t available yet
  • ➖ Adds protocol/version negotiation complexity now, for limited immediate value

Recommendation: The PR’s approach (BiDi-first API with a Firefox classic fallback, plus a temporary Chromium override) is the most pragmatic given current protocol gaps (chromium-bidi lacking base64/archive support). The CDP disablement is reasonable if the transport is truly shared, but consider documenting a future path (or explicit guardrails) for re-enabling CDP when a separate transport becomes feasible.

Files changed (24) +491 / -42

Enhancement (9) +147 / -6
features.rbOverride install_web_extension for Chromium path-only install +6/-0

Override install_web_extension for Chromium path-only install

• Adds a temporary Chromium-specific implementation of install_web_extension that passes an unpacked local directory path through the BiDi webExtension API. Includes a TODO to remove the override once chromium-bidi supports base64/archive installs.

rb/lib/selenium/webdriver/chromium/features.rb

options.rbAdd BiDi-required Chromium flags for extension debugging +4/-0

Add BiDi-required Chromium flags for extension debugging

• When BiDi is enabled, injects --enable-unsafe-extension-debugging and --remote-debugging-pipe into Chrome options args. Uses set-union semantics to avoid duplicates.

rb/lib/selenium/webdriver/chromium/options.rb

common.rbRequire WebExtension handle class +1/-0

Require WebExtension handle class

• Loads the new common/web_extension implementation as part of the shared WebDriver requires.

rb/lib/selenium/webdriver/common.rb

driver.rbExpose Driver#install_web_extension and #uninstall_web_extension APIs +28/-0

Expose Driver#install_web_extension and #uninstall_web_extension APIs

• Adds public Driver methods with YARD docs describing BiDi requirements and cross-browser behavior differences. Delegates to the underlying bridge and returns/accepts a WebExtension handle.

rb/lib/selenium/webdriver/common/driver.rb

has_addons.rbDeprecate legacy Firefox install_addon/uninstall_addon methods +2/-0

Deprecate legacy Firefox install_addon/uninstall_addon methods

• Emits deprecation warnings directing users to install_web_extension and uninstall_web_extension while still calling the legacy bridge methods for backward compatibility.

rb/lib/selenium/webdriver/common/driver_extensions/has_addons.rb

web_extension.rbAdd WebExtension value object wrapper +42/-0

Add WebExtension value object wrapper

• Introduces Selenium::WebDriver::WebExtension to wrap the browser-assigned extension identifier for later uninstall calls.

rb/lib/selenium/webdriver/common/web_extension.rb

features.rbImplement install/uninstall web extension for Firefox with BiDi and classic fallback +29/-0

Implement install/uninstall web extension for Firefox with BiDi and classic fallback

• Adds Firefox support for install_web_extension with optional permanent/allow_private_browsing, using BiDi moz vendor commands when available. Falls back to the classic moz/addon endpoint when BiDi is disabled, including a clear error when allow_private_browsing is explicitly false without BiDi.

rb/lib/selenium/webdriver/firefox/features.rb

bidi_bridge.rbAdd BiDi bridge webExtension protocol integration +15/-0

Add BiDi bridge webExtension protocol integration

• Implements install_web_extension and uninstall_web_extension using the generated BiDi Protocol::WebExtension client and base64 encoding via encode_extension. Lazily instantiates the web_extension protocol object.

rb/lib/selenium/webdriver/remote/bidi_bridge.rb

bridge.rbGate web extension APIs behind BiDi and add shared extension encoding helper +20/-6

Gate web extension APIs behind BiDi and add shared extension encoding helper

• Refactors BiDi gating to a single bidi(*) method and aliases BiDi-only methods (connection/web_extension/install/uninstall) to raise a consistent enable-BiDi error when disabled. Adds bidi? predicate and a private encode_extension helper supporting directory zip, file base64, or passthrough base64 bytes.

rb/lib/selenium/webdriver/remote/bridge.rb

Bug fix (1) +6 / -0
has_devtools.rbDisable DevTools (CDP) API when BiDi is enabled +6/-0

Disable DevTools (CDP) API when BiDi is enabled

• Adds a guard that raises a WebDriverError if driver.devtools is called while BiDi is active, clarifying that CDP shares a transport with BiDi.

rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb

Tests (6) +269 / -36
devtools_spec.rbAdd integration coverage for CDP disabled under BiDi +8/-0

Add integration coverage for CDP disabled under BiDi

• Adds a Chromium-only integration spec verifying driver.devtools raises when BiDi is enabled.

rb/spec/integration/selenium/webdriver/devtools_spec.rb

driver_spec.rbAdd BiDi-enabled integration tests for extension install/uninstall +95/-0

Add BiDi-enabled integration tests for extension install/uninstall

• Introduces end-to-end tests for install_web_extension across directory and archive inputs, with Chromium remote cases marked pending due to path-only support. Adds Firefox-specific assertions for permanent/base64 inputs and private-browsing behavior.

rb/spec/integration/selenium/webdriver/driver_spec.rb

driver_spec.rbRestructure Firefox integration specs and add non-BiDi web extension tests +86/-36

Restructure Firefox integration specs and add non-BiDi web extension tests

• Reorganizes tests to separate non-BiDi-only cases and adds Firefox coverage for install_web_extension without BiDi (classic endpoint). Updates install_addon spec to assert deprecation warnings and adds private-browsing error coverage when disabling it without BiDi.

rb/spec/integration/selenium/webdriver/firefox/driver_spec.rb

options_spec.rbUnit test Chromium BiDi flags injection for extension debugging +18/-0

Unit test Chromium BiDi flags injection for extension debugging

• Adds unit tests asserting BiDi-enabled Chrome options include the required debugging flags, do not add them when BiDi is off, and avoid duplicates.

rb/spec/unit/selenium/webdriver/chrome/options_spec.rb

web_extension_spec.rbUnit test WebExtension id accessor +32/-0

Unit test WebExtension id accessor

• Adds a minimal spec ensuring WebExtension exposes the browser-assigned id.

rb/spec/unit/selenium/webdriver/common/web_extension_spec.rb

bridge_spec.rbUnit test BiDi-gated error messages for web extension APIs +30/-0

Unit test BiDi-gated error messages for web extension APIs

• Adds tests verifying helpful errors when install/uninstall web extension methods are called without BiDi, including the Firefox-specific private-browsing disable error path.

rb/spec/unit/selenium/webdriver/remote/bridge_spec.rb

Other (8) +69 / -0
bridge.rbsExtend Bridge interface with BiDi and web extension helpers +6/-0

Extend Bridge interface with BiDi and web extension helpers

• Adds type signatures for bidi?, web_extension, and encode_extension on the bridge interface.

rb/sig/interfaces/bridge.rbs

features.rbsAdd Chromium features signature for install_web_extension +2/-0

Add Chromium features signature for install_web_extension

• Declares the install_web_extension return type as WebExtension for Chromium features.

rb/sig/lib/selenium/webdriver/chromium/features.rbs

driver.rbsAdd Driver signatures for web extension install/uninstall +4/-0

Add Driver signatures for web extension install/uninstall

• Adds type signatures for install_web_extension (with options) and uninstall_web_extension (taking WebExtension).

rb/sig/lib/selenium/webdriver/common/driver.rbs

web_extension.rbsAdd RBS for WebExtension handle class +29/-0

Add RBS for WebExtension handle class

• Introduces the RBS definition for Selenium::WebDriver::WebExtension and its id accessor.

rb/sig/lib/selenium/webdriver/common/web_extension.rbs

features.rbsAdd Firefox features signatures for install/uninstall web extension +8/-0

Add Firefox features signatures for install/uninstall web extension

• Adds typed definitions for install_web_extension/uninstall_web_extension and the private classic_install_web_extension helper.

rb/sig/lib/selenium/webdriver/firefox/features.rbs

bidi_bridge.rbsAdd BiDiBridge signatures for webExtension protocol and install/uninstall +8/-0

Add BiDiBridge signatures for webExtension protocol and install/uninstall

• Declares the web_extension protocol field and the install/uninstall web extension methods on the BiDi bridge.

rb/sig/lib/selenium/webdriver/remote/bidi_bridge.rbs

bridge.rbsAdd bridge signatures for BiDi gating and web extension APIs +10/-0

Add bridge signatures for BiDi gating and web extension APIs

• Adds typed declarations for bidi?, install_web_extension/uninstall_web_extension, plus private web_extension and encode_extension helpers.

rb/sig/lib/selenium/webdriver/remote/bridge.rbs

BUILD.bazelEnable BiDi for driver integration specs in Bazel +2/-0

Enable BiDi for driver integration specs in Bazel

• Adds driver_spec.rb to the set of BiDi-implementation specs and sets bidi=True for integration test targets so new BiDi-based extension tests run.

rb/spec/integration/selenium/webdriver/BUILD.bazel

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. devtools raises under BiDi 📘 Rule violation ⚙ Maintainability
Description
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.
Code

rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb[R32-35]

+          if @bridge.bidi?
+            raise Error::WebDriverError,
+                  'CDP (DevTools) is disabled when BiDi is enabled; use the WebDriver BiDi APIs instead'
+          end
Evidence
PR Compliance ID 1 requires preserving public behavior by default. The changed devtools method now
explicitly raises an error whenever BiDi is enabled, making driver.devtools unusable in that
configuration.

AGENTS.md: Maintain API/ABI Compatibility by Default
rb/lib/selenium/webdriver/common/driver_extensions/has_devtools.rb[28-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. BiDi args NoMethodError 🐞 Bug ☼ Reliability
Description
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.
Code

rb/lib/selenium/webdriver/chromium/options.rb[R239-241]

+          if bidi?
+            options['args'] = options['args'].to_a | %w[--enable-unsafe-extension-debugging --remote-debugging-pipe]
+          end
Evidence
process_browser_options unconditionally calls options['args'].to_a under BiDi, but args can be
overridden to a non-Array via the options merge and via add_option, so BiDi-enabled sessions can
fail locally with NoMethodError.

rb/lib/selenium/webdriver/chromium/options.rb[227-241]
rb/lib/selenium/webdriver/chromium/options.rb[72-85]
rb/lib/selenium/webdriver/common/options.rb[93-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

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

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-build Includes scripting, bazel and CI integrations C-rb Ruby Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants