Table of Contents

WebExtension Module

The WebExtension module allows you to manage browser extensions programmatically, enabling automated testing of extension functionality.

Overview

The WebExtension module allows you to:

  • Install browser extensions
  • Uninstall extensions
  • Test extension functionality
  • Automate extension-based workflows

Accessing the Module

WebExtensionModule webExtension = driver.WebExtension;

Timeout and Cancellation

All commands in this module accept optional timeoutOverride and CancellationToken parameters. Use timeoutOverride to set a per-command timeout (defaults to BiDiDriver.DefaultCommandTimeout when omitted). Use CancellationToken for cooperative cancellation. See the API Design Guide for details and examples.

Installing Extensions

InstallAsync accepts one of three ExtensionData forms: ExtensionArchivePath (a packed extension archive file), ExtensionPath (an unpacked extension directory), or ExtensionBase64Encoded (a packed extension supplied inline as a base64-encoded string).

Install from Archive Path

InstallCommandParameters @params = new InstallCommandParameters(
    new ExtensionArchivePath("/path/to/extension.zip"));

InstallCommandResult result = await driver.WebExtension.InstallAsync(@params);
string extensionId = result.ExtensionId;

Console.WriteLine($"Extension installed: {extensionId}");

Install Unpacked Extension

// Install from unpacked extension directory
InstallCommandParameters @params = new InstallCommandParameters(
    new ExtensionPath("/path/to/extension-directory"));

InstallCommandResult result = await driver.WebExtension.InstallAsync(@params);
Console.WriteLine($"Extension installed: {result.ExtensionId}");

Uninstalling Extensions

Uninstall by ID

UninstallCommandParameters @params = new UninstallCommandParameters(extensionId);
await driver.WebExtension.UninstallAsync(@params);

Console.WriteLine("Extension uninstalled");

Common Patterns

Testing with Extension

// Install extension
InstallCommandParameters installParams = new InstallCommandParameters(
    new ExtensionPath("/path/to/my-extension"));

InstallCommandResult installResult =
    await driver.WebExtension.InstallAsync(installParams);
string extensionId = installResult.ExtensionId;

Console.WriteLine($"Extension installed: {extensionId}");

// Navigate and test
await driver.BrowsingContext.NavigateAsync(
    new NavigateCommandParameters(contextId, "https://example.com")
    { Wait = ReadinessState.Complete });

// Test extension functionality
EvaluateResult result = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        "document.querySelector('[data-extension-injected]') !== null",
        new ContextTarget(contextId),
        true));

if (result is EvaluateResultSuccess success &&
    success.Result is BooleanRemoteValue boolValue)
{
    bool extensionActive = boolValue.Value;
    Console.WriteLine($"Extension active: {extensionActive}");
}

// Clean up
await driver.WebExtension.UninstallAsync(
    new UninstallCommandParameters(extensionId));

Testing Extension Content Scripts

// Install extension with content script
InstallCommandParameters @params = new InstallCommandParameters(
    new ExtensionPath("/path/to/content-script-extension"));

InstallCommandResult result = await driver.WebExtension.InstallAsync(@params);

// Navigate to test page
await driver.BrowsingContext.NavigateAsync(
    new NavigateCommandParameters(contextId, "https://example.com")
    { Wait = ReadinessState.Complete });

await Task.Delay(1000);

// Check if content script modified the page
EvaluateResult evalResult = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        "document.body.dataset.contentScriptLoaded",
        new ContextTarget(contextId),
        true));

if (evalResult is EvaluateResultSuccess success &&
    success.Result is StringRemoteValue loadedValue)
{
    string loaded = loadedValue.Value;
    Console.WriteLine($"Content script loaded: {loaded}");
}

// Clean up
await driver.WebExtension.UninstallAsync(
    new UninstallCommandParameters(result.ExtensionId));

Testing Multiple Extensions

List<string> extensionIds = new List<string>();

// Install multiple extensions
string[] extensionPaths = new[]
{
    "/path/to/extension1",
    "/path/to/extension2",
    "/path/to/extension3"
};

foreach (string path in extensionPaths)
{
    InstallCommandParameters @params = new InstallCommandParameters(
        new ExtensionPath(path));

    InstallCommandResult result = await driver.WebExtension.InstallAsync(@params);
    extensionIds.Add(result.ExtensionId);

    Console.WriteLine($"Installed: {result.ExtensionId}");
}

// Run tests with all extensions active
// ...

// Clean up all extensions
foreach (string id in extensionIds)
{
    await driver.WebExtension.UninstallAsync(
        new UninstallCommandParameters(id));
}

Testing Extension Permissions

// Install extension that requires permissions
InstallCommandParameters @params = new InstallCommandParameters(
    new ExtensionPath("/path/to/permission-extension"));

InstallCommandResult result = await driver.WebExtension.InstallAsync(@params);

// Navigate to page where extension needs permissions
await driver.BrowsingContext.NavigateAsync(
    new NavigateCommandParameters(contextId, "https://example.com")
    { Wait = ReadinessState.Complete });

// Test that extension has required permissions
EvaluateResult evalResult = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        @"new Promise((resolve) => {
            chrome.permissions.contains({
                permissions: ['storage']
            }, (result) => resolve(result));
        })",
        new ContextTarget(contextId),
        true));

if (evalResult is EvaluateResultSuccess success &&
    success.Result is BooleanRemoteValue hasPermissionValue)
{
    bool hasPermission = hasPermissionValue.Value;
    Console.WriteLine($"Extension has storage permission: {hasPermission}");
}

// Uninstall
await driver.WebExtension.UninstallAsync(
    new UninstallCommandParameters(result.ExtensionId));

Extension Formats

The protocol defines three forms, and both archive forms are plain zip archives rather than browser-specific packaging. The remote end extracts the archive before installing.

  • ExtensionArchivePath: the path, on the remote end's file system, to a zip archive containing the extension.
  • ExtensionBase64Encoded: the same zip archive, base64-encoded, for when the file is not on the remote end's file system.
  • ExtensionPath: a directory containing manifest.json and the extension files — an unpacked extension.

A browser-specific package such as a Chrome .crx or a Firefox .xpi is not one of these forms. Repackage the extension as a zip archive, or install it unpacked from a directory.

Safari

  • App Extensions: Safari extensions are packaged differently
  • Limited BiDi support: WebExtension module has limited Safari support

Browser Support

Browser Support Level Format
Chrome/Edge ✅ Full support zip archive, unpacked
Firefox ⚠️ Different API zip archive, unpacked
Safari ⚠️ Limited App extensions

Best Practices

  1. Use unpacked extensions for development: Easier to modify and debug
  2. Store extension ID: Save the ID returned from InstallAsync for cleanup
  3. Wait for extension initialization: Add delays after installation if needed
  4. Clean up after tests: Always uninstall extensions to prevent conflicts
  5. Test with real extension builds: Use production-ready extension packages

Common Issues

Extension Installation Fails

Problem: Cannot install browser extension.

Solution:

  • Verify extension path is correct and absolute
  • Check extension file format (a zip archive, or a directory for an unpacked extension)
  • Ensure manifest.json is valid
  • Try loading as unpacked extension for development
  • Check browser console for extension errors

Extension Not Active

Problem: Extension installs but doesn't work.

Solution:

  • Wait for extension to initialize after installation
  • Check if page URL matches extension's content script patterns
  • Verify extension permissions in manifest.json
  • Reload the page after extension installation

Extension ID Not Found

Problem: Cannot uninstall extension by ID.

Solution:

  • Save the extension ID returned from InstallAsync
  • Don't manually construct extension IDs
  • Ensure extension is still installed before uninstalling

Manifest Version Issues

Problem: Extension manifest version incompatible.

Solution:

  • Use Manifest V3 for Chrome/Edge (preferred)
  • Manifest V2 support varies by browser version
  • Update extension manifest to supported version
  • Check browser's extension documentation

Next Steps

Further Reading