Skip to content

[js][bidi] Add support for installing and uninstalling extension #16166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: trunk
Choose a base branch
from

Conversation

pujagani
Copy link
Contributor

@pujagani pujagani commented Aug 12, 2025

User description

🔗 Related Issues

Related to #15585

💥 What does this PR do?

Added Extension BiDi module to JS bindings

🔧 Implementation Notes

💡 Additional Considerations

🔄 Types of changes

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

PR Type

Enhancement


Description

  • Add WebExtension BiDi module for JavaScript bindings

  • Support installing extensions from path, archive, or base64

  • Implement uninstall functionality with error handling

  • Include comprehensive test coverage for all methods


Diagram Walkthrough

flowchart LR
  ExtensionData["ExtensionData Class"] --> WebExtension["WebExtension Module"]
  WebExtension --> Install["install() method"]
  WebExtension --> Uninstall["uninstall() method"]
  Install --> BiDi["BiDi Protocol"]
  Uninstall --> BiDi
  Tests["Extension Tests"] --> WebExtension
Loading

File Walkthrough

Relevant files
Enhancement
extensionData.js
Add ExtensionData class for extension configuration           

javascript/selenium-webdriver/bidi/webExtension/extensionData.js

  • Create ExtensionData class with private fields for type and path
  • Add static factory methods for path, archive path, and base64 data
  • Implement asMap() method for BiDi protocol serialization
+53/-0   
webExtension.js
Implement WebExtension BiDi module with install/uninstall methods

javascript/selenium-webdriver/bidi/webExtension/webExtension.js

  • Implement WebExtension class with BiDi protocol integration
  • Add install() method supporting ExtensionData parameter validation
  • Add uninstall() method with error handling for invalid extension IDs
  • Include helper function for instance creation and initialization
+100/-0 
Tests
extension_test.js
Add comprehensive test coverage for WebExtension module   

javascript/selenium-webdriver/test/bidi/extension_test.js

  • Add comprehensive test suite for WebExtension functionality
  • Test installation from path, archive path, and base64 encoding
  • Test uninstall functionality and error handling for non-existent
    extensions
  • Configure tests to run specifically on Firefox browser
+96/-0   
Configuration changes
BUILD.bazel
Update build configuration for WebExtension module             

javascript/selenium-webdriver/BUILD.bazel

  • Add bidi/webExtension/*.js pattern to js_library sources
+1/-0     

@selenium-ci selenium-ci added C-nodejs JavaScript Bindings B-build Includes scripting, bazel and CI integrations B-devtools Includes everything BiDi or Chrome DevTools related labels Aug 12, 2025
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

15585 - PR Code Verified

Compliant requirements:

  • Expose WebDriver BiDi command to install web extensions.
  • Expose WebDriver BiDi command to uninstall web extensions.
  • Support multiple extension input formats (e.g., file path, archive, base64) aligned with BiDi spec payloads.
  • Integrate with existing Selenium JS bindings (driver validation and BiDi session usage).
  • Provide automated tests covering install and uninstall operations.
  • Update build configuration to include new module.

Requires further human verification:

  • Cross-browser verification beyond Firefox (e.g., Chrome) per spec support and driver capabilities.
  • Documentation updates and public API surface confirmation (naming, exports) across packages.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Robustness

Ensure init() is called before install/uninstall to avoid using an uninitialized this.bidi; consider guarding or initializing in constructor or throwing a clearer error if not initialized.

async init() {
  if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
    throw Error('WebDriver instance must support BiDi protocol')
  }

  this.bidi = await this._driver.getBidi()
}
API Naming

The static constructors setPath, setArchivePath, and setBase64Encoded are semantically "create" methods; consider clearer names like fromPath, fromArchive, fromBase64 for consistency with common factory patterns.

static setPath(path) {
  return new ExtensionData("path", path)
}

static setArchivePath(path) {
  return new ExtensionData("archivePath", path)
}

static setBase64Encoded(value) {
  return new ExtensionData("base64", value)
}
Error Handling

install() assumes response.result.extension exists; add validation and throw a descriptive error if the BiDi response shape differs or contains an error.

  let response = await this.bidi.send(command)
  return response.result.extension
}

Copy link
Contributor

qodo-merge-pro bot commented Aug 12, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
BiDi capability detection robustness

The BiDi support check relies solely on the presence of webSocketUrl, which may
be absent with alternative BiDi transports or future capability shapes, causing
false negatives. Consider using the driver's explicit BiDi availability (e.g., a
dedicated capability/feature flag or catching getBidi failures) to robustly
detect and initialize BiDi across browsers and vendors.

Examples:

javascript/selenium-webdriver/bidi/extension/extension.js [25-31]
async init() {
  if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
    throw Error('WebDriver instance must support BiDi protocol')
  }

  this.bidi = await this._driver.getBidi()
}

Solution Walkthrough:

Before:

async init() {
  const capabilities = await this._driver.getCapabilities();
  if (!capabilities.get('webSocketUrl')) {
    throw Error('WebDriver instance must support BiDi protocol');
  }
  this.bidi = await this._driver.getBidi();
}

After:

async init() {
  try {
    this.bidi = await this._driver.getBidi();
  } catch (e) {
    // Potentially inspect 'e' to ensure it's the expected error
    throw Error('WebDriver instance must support BiDi protocol');
  }
}
Suggestion importance[1-10]: 7

__

Why: This is a strong design suggestion that correctly identifies a brittle check and proposes a more robust, future-proof alternative, improving the module's resilience to changes in driver implementations.

Medium
General
Cache capabilities and standardize error
Suggestion Impact:The commit changed the error construction to use new Error(...), matching part of the suggestion. It did not cache getCapabilities(); that call remained inline.

code diff:

-  async init() {
-    if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
-      throw Error('WebDriver instance must support BiDi protocol')
-    }
-
-    this.bidi = await this._driver.getBidi()
-  }

Cache the capabilities to avoid two round-trips and ensure consistent checks.
Also, use a consistent error construction style with new Error(...) for clarity.

javascript/selenium-webdriver/bidi/extension/extension.js [25-31]

 async init() {
-  if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
-    throw Error('WebDriver instance must support BiDi protocol')
+  const caps = await this._driver.getCapabilities()
+  if (!caps.get('webSocketUrl')) {
+    throw new Error('WebDriver instance must support BiDi protocol')
   }
-
   this.bidi = await this._driver.getBidi()
 }

[Suggestion processed]

Suggestion importance[1-10]: 3

__

Why: The suggestion improves code style by using new Error() and makes the code slightly more readable by caching getCapabilities(), but the performance claim of avoiding two round-trips is incorrect as it's only called once.

Low
  • Update

@pujagani pujagani marked this pull request as draft August 13, 2025 09:41
@pujagani pujagani marked this pull request as ready for review August 14, 2025 10:46
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

15585 - Partially compliant

Compliant requirements:

  • Expose WebDriver BiDi commands to install web extensions.
  • Expose WebDriver BiDi commands to uninstall web extensions.
  • Provide API surface in Selenium (JS bindings) to call these commands.
  • Handle inputs for extension data per spec (path, archive, base64).
  • Include appropriate tests and documentation (tests added).

Non-compliant requirements:

  • Support Chrome where BiDi is implemented. (Tests restricted to Firefox only; Chrome support not validated here)

Requires further human verification:

  • Verify behavior on Chrome (and other BiDi-capable browsers) in CI or manual runs.
  • Confirm alignment with latest BiDi spec error semantics and response shapes across browsers.
⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Error Handling

The uninstall path throws only when response.type === 'error', but BiDi client may throw on send() or return different shapes across browsers. Validate that error propagation and response parsing match the BiDi client used and both Firefox and Chrome behaviors.

async uninstall(id) {
  const command = {
    method: 'webExtension.uninstall',
    params: {
      extension: id,
    },
  }

  const response = await this.bidi.send(command)

  if (response.type === 'error') {
    throw new Error(`${response.error}: ${response.message}`)
  }
}
API Exposure

The module exports a factory function returning an initialized instance. Ensure this aligns with existing BiDi module patterns and that tree-shaking/build configs include it. Consider also exporting the class for advanced use if consistent with codebase conventions.

/**
 * Helper to create and initialize an Extension instance.
 *
 * @param {import('selenium-webdriver').WebDriver} driver
 *   A Selenium WebDriver instance.
 * @returns {Promise<WebExtension>}
 *   An Extension instance.
 */
async function getWebExtensionInstance(driver) {
  let instance = new WebExtension(driver)
  await instance.init()
  return instance
}

module.exports = getWebExtensionInstance
Naming Consistency

The internal field #path stores either a path or base64 value. Consider renaming to a neutral name (e.g., #value) to avoid confusion and future misuse; verify with serialization map.

class ExtensionData {
  #type
  #path

  constructor(type, path) {
    this.#type = type
    this.#path = path
  }

  static setPath(path) {
    return new ExtensionData('path', path)
  }

  static setArchivePath(path) {
    return new ExtensionData('archivePath', path)
  }

  static setBase64Encoded(value) {
    return new ExtensionData('base64', value)
  }

  asMap() {
    let toReturn = {}
    toReturn['type'] = this.#type

    if (this.#type === 'base64') {
      toReturn['value'] = this.#path
    } else {
      toReturn['path'] = this.#path
    }

    return toReturn
  }
}

module.exports = ExtensionData

Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against malformed responses

Validate the returned payload defensively to avoid runtime errors if the BiDi
response shape differs or on partial failures. Throw a clear error when result
or extension is missing before accessing it.

javascript/selenium-webdriver/bidi/webExtension/webExtension.js [46-60]

 async install(extensionData) {
   if (!(extensionData instanceof ExtensionData)) {
     throw new Error('install() requires an ExtensionData instance')
   }
 
   const command = {
     method: 'webExtension.install',
     params: {
       extensionData: extensionData.asMap(),
     },
   }
 
-  let response = await this.bidi.send(command)
-  return response.result.extension
+  const response = await this.bidi.send(command)
+  const extensionId = response && response.result && response.result.extension
+  if (!extensionId) {
+    throw new Error('Install failed: missing extension id in response')
+  }
+  return extensionId
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential runtime error by adding defensive checks for the response payload, which improves the robustness of the install method.

Medium
Let transport surface errors

Avoid relying on a non-standard response.type contract and let this.bidi.send
surface protocol errors via rejection. Removing the manual error check prevents
masking of results and double-handling. If needed, add minimal validation of id
input to catch obvious misuse early.

javascript/selenium-webdriver/bidi/webExtension/webExtension.js [70-83]

 async uninstall(id) {
+  if (!id || typeof id !== 'string') {
+    throw new Error('uninstall() requires a non-empty string id')
+  }
+
   const command = {
     method: 'webExtension.uninstall',
     params: {
       extension: id,
     },
   }
 
-  const response = await this.bidi.send(command)
-
-  if (response.type === 'error') {
-    throw new Error(`${response.error}: ${response.message}`)
-  }
+  await this.bidi.send(command)
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly proposes to rely on the bidi.send method to handle protocol errors via promise rejection, which simplifies the code and follows common async patterns.

Low
General
Validate base64 input early

Ensure non-empty Base64 data is provided to prevent sending invalid payloads to
the BiDi endpoint. Add a basic type/emptiness check and surface a clear error
early.

javascript/selenium-webdriver/bidi/webExtension/extensionData.js [35-37]

 static setBase64Encoded(value) {
+  if (typeof value !== 'string' || value.length === 0) {
+    throw new Error('setBase64Encoded() requires a non-empty base64 string')
+  }
   return new ExtensionData('base64', value)
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion adds useful input validation to prevent sending empty or invalid data to the BiDi endpoint, improving robustness with a fail-fast approach.

Low
  • More

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 B-devtools Includes everything BiDi or Chrome DevTools related C-nodejs JavaScript Bindings Review effort 2/5
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants