Table of Contents

Your First WebDriverBiDi Application

This tutorial walks you through creating a complete WebDriverBiDi.NET application from scratch.

Prerequisites

  • .NET SDK 8.0 or higher installed, to build and run the console application this tutorial walks through. The library itself needs only a runtime compatible with .NET Standard 2.0
  • Firefox, which the application connects to (see Using Chrome or Edge for those browsers)
  • Basic knowledge of C# and async/await

Step 1: Create the Project

Open a terminal and create a new console application:

mkdir MyFirstBiDiApp
cd MyFirstBiDiApp
dotnet new console

Step 2: Add the NuGet Packages

Add the WebDriverBiDi package, the protocol client:

dotnet add package WebDriverBiDi

Step 3: Write the Application

Replace the contents of Program.cs with the code below, adding these using directives at the top of the file:

using WebDriverBiDi;
using WebDriverBiDi.BrowsingContext;
using WebDriverBiDi.Log;
using WebDriverBiDi.Protocol;
using WebDriverBiDi.Script;
using WebDriverBiDi.Session;
// Create a driver with a 30-second command timeout
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));

try
{
    // Connect to Firefox, started with --remote-debugging-port=9222, and create a session
    Console.WriteLine("Connecting to browser...");
    await driver.StartAsync("ws://localhost:9222/session");
    await driver.Session.NewSessionAsync(new NewCommandParameters());

    Console.WriteLine("Connected!");

    // Set up console log monitoring
    driver.Log.OnEntryAdded.AddObserver((EntryAddedEventArgs e) =>
    {
        Console.WriteLine($"[Browser Console] {e.Level}: {e.Text}");
    });

    SubscribeCommandParameters subscribe =
        new SubscribeCommandParameters(driver.Log.OnEntryAdded.EventName);
    await driver.Session.SubscribeAsync(subscribe);

    // Get the current browsing context
    Console.WriteLine("\nGetting browsing contexts...");
    GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
        new GetTreeCommandParameters());

    string contextId = tree.ContextTree[0].BrowsingContextId;
    Console.WriteLine($"Active context: {contextId}");

    // Navigate to a website
    Console.WriteLine("\nNavigating to example.com...");
    NavigateCommandParameters navParams = new NavigateCommandParameters(
        contextId,
        "https://example.com")
    {
        Wait = ReadinessState.Complete
    };

    NavigateCommandResult navResult = await driver.BrowsingContext.NavigateAsync(navParams);
    Console.WriteLine($"Navigation complete! URL: {navResult.Url}");

    // Get the page title
    Console.WriteLine("\nGetting page title...");
    EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
        "document.title",
        new ContextTarget(contextId),
        true);

    EvaluateResult scriptResult = await driver.Script.EvaluateAsync(evalParams);

    if (scriptResult is EvaluateResultSuccess success &&
        success.Result is StringRemoteValue titleValue)
    {
        string title = titleValue.Value ?? "No title";
        Console.WriteLine($"Page title: {title}");
    }

    // Get page information
    Console.WriteLine("\nGetting page information...");
    // Parenthesized: a script beginning with a brace parses as a block, not an object literal.
    string infoScript = @"
    ({
        url: window.location.href,
        linkCount: document.querySelectorAll('a').length,
        headingCount: document.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
        paragraphCount: document.querySelectorAll('p').length
    })";

    EvaluateCommandParameters infoParams = new EvaluateCommandParameters(
        infoScript,
        new ContextTarget(contextId),
        true);

    EvaluateResult infoResult = await driver.Script.EvaluateAsync(infoParams);

    if (infoResult is EvaluateResultSuccess infoSuccess &&
        infoSuccess.Result is KeyValuePairCollectionRemoteValue infoValue &&
        infoValue.Value is RemoteValueDictionary info)
    {
        Console.WriteLine("Page Analysis:");
        Console.WriteLine($"  URL: {info["url"].As<StringRemoteValue>().Value}");
        Console.WriteLine($"  Links: {info["linkCount"].As<NumberRemoteValue>().Value}");
        Console.WriteLine($"  Headings: {info["headingCount"].As<NumberRemoteValue>().Value}");
        Console.WriteLine($"  Paragraphs: {info["paragraphCount"].As<NumberRemoteValue>().Value}");
    }

    // Take a screenshot
    Console.WriteLine("\nCapturing screenshot...");
    CaptureScreenshotCommandParameters screenshotParams =
        new CaptureScreenshotCommandParameters(contextId);

    CaptureScreenshotCommandResult screenshot =
        await driver.BrowsingContext.CaptureScreenshotAsync(screenshotParams);

    byte[] imageBytes = Convert.FromBase64String(screenshot.Data);
    await File.WriteAllBytesAsync("example-screenshot.png", imageBytes);
    Console.WriteLine("Screenshot saved to example-screenshot.png");

    Console.WriteLine("\n✓ All operations completed successfully!");
    Console.WriteLine("\nPress any key to exit...");
    Console.ReadKey();
}
catch (WebDriverBiDiException ex)
{
    Console.WriteLine($"\n✗ WebDriver BiDi Error: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"\n✗ Unexpected Error: {ex.Message}");
}
finally
{
    // Disconnect; the browser keeps running until you close it
    Console.WriteLine("\nDisconnecting from browser...");
    await driver.StopAsync();
    Console.WriteLine("Disconnected!");
}

Step 4: Run the Application

Start Firefox with its remote debugging port, on which it serves WebDriver BiDi:

firefox --remote-debugging-port=9222

Then run the application:

dotnet run

You should see output similar to:

Connecting to browser...
Connected!

Getting browsing contexts...
Active context: ABC123

Navigating to example.com...
Navigation complete! URL: https://example.com/

Getting page title...
Page title: Example Domain

Getting page information...
Page Analysis:
  URL: https://example.com/
  Links: 1
  Headings: 1
  Paragraphs: 2

Capturing screenshot...
Screenshot saved to example-screenshot.png

✓ All operations completed successfully!

Press any key to exit...

Understanding the Code

1. Connecting

BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync("ws://localhost:9222/session");
await driver.Session.NewSessionAsync(new NewCommandParameters());

The driver connects to the WebDriver BiDi endpoint Firefox serves at /session on its remote debugging port. No session exists there yet, so the application creates one.

The driver has a 30-second command timeout, overriding the library's default, BiDiDriver.DefaultCommandWaitTimeout (60 seconds); adjust the value to suit your environment.

Tip: By default, event handler exceptions and problems with messages from the browser never throw. They are reported only through the driver's diagnostic events, which nothing may be watching. During development, set the error behaviors to TransportErrorBehavior.Terminate so that problems surface as exceptions:

driver.TransportConfiguration.EventHandlerExceptionBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.ProtocolErrorBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.UnknownMessageBehavior = TransportErrorBehavior.Terminate;
driver.TransportConfiguration.UnexpectedErrorBehavior = TransportErrorBehavior.Terminate;

See Error Handling for a full explanation of the four error behavior properties and the recommended settings for production use.

2. Event Subscription

driver.Log.OnEntryAdded.AddObserver((EntryAddedEventArgs e) => { /* handle log */ });
SubscribeCommandParameters subscribe = new SubscribeCommandParameters(
    driver.Log.OnEntryAdded.EventName);
await driver.Session.SubscribeAsync(subscribe);

Sets up monitoring for browser console logs before they occur.

3. Getting the Context

GetTreeCommandResult tree = await driver.BrowsingContext.GetTreeAsync(
    new GetTreeCommandParameters());
string contextId = tree.ContextTree[0].BrowsingContextId;

Retrieves the current browsing contexts (tabs). We use the first one.

4. Navigation

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

Navigates to a URL and waits for the page to fully load.

5. JavaScript Execution

EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
    "document.title",
    new ContextTarget(contextId),
    true);
EvaluateResult result = await driver.Script.EvaluateAsync(evalParams);

Executes JavaScript and retrieves the result.

6. Screenshot Capture

CaptureScreenshotCommandParameters screenshotParams =
    new CaptureScreenshotCommandParameters(contextId);
CaptureScreenshotCommandResult screenshot =
    await driver.BrowsingContext.CaptureScreenshotAsync(screenshotParams);
byte[] imageBytes = Convert.FromBase64String(screenshot.Data);
await File.WriteAllBytesAsync("screenshot.png", imageBytes);

Captures a screenshot and saves it to disk.

Using Chrome or Edge

Chrome and Edge serve WebDriver BiDi through their driver executables rather than on their own debugging port: run chromedriver --port=9515 (or msedgedriver) and create a session with the webSocketUrl capability. Browser Setup walks through this. Then the application connects to the session's webSocketUrl:

// The webSocketUrl of a session created through chromedriver, which already exists,
// so the application does not call Session.NewSessionAsync.
string webSocketUrl = "ws://localhost:9515/session/YOUR-SESSION-ID";

BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));
await driver.StartAsync(webSocketUrl);

The session already exists, so the application does not create one; the rest of it is unchanged. To download and launch browsers from code, see Dramaturge.Browsers.

Common Issues and Solutions

"Could not connect to remote WebSocket server"

Problem: Nothing is listening at the WebSocket URL, or the URL is wrong. StartAsync retries the connection every 500 milliseconds until the startup timeout (10 seconds by default) runs out, then throws WebDriverBiDiTimeoutException.

Solution:

  • Ensure Firefox is running with --remote-debugging-port=9222, or, for Chrome or Edge, that the driver is running and the session was created
  • For a driver, verify it is listening by visiting http://localhost:9515/status
  • Check that no firewall is blocking the port
  • Make sure the URL is the webSocketUrl from the session response, not Chrome's /devtools/browser/… CDP URL

"Timed out executing command"

Problem: The command took longer than the timeout period, so it failed with WebDriverBiDiTimeoutException.

Solution:

  • Increase the timeout: new BiDiDriver(TimeSpan.FromSeconds(60))
  • Check your network connection
  • Ensure the target website is accessible

"no such frame"

Problem: The command failed with a WebDriverBiDiCommandException whose ErrorCode is NoSuchFrame: the browsing context ID is invalid or the tab was closed.

Solution:

  • Always get fresh context IDs before using them
  • Don't cache context IDs across navigations that might close tabs

Next Steps

Now that you have a working application, explore these topics:

  1. Core Concepts - Understand modules, commands, and events
  2. Events and Observables - Master event handling
  3. Module Guides - Learn about specific modules
  4. Common Scenarios - See more examples

Full Example Repository

Find complete examples and more advanced scenarios in the project's demo applications:

  • src/WebDriverBiDi.Demo/ - Various demonstration scenarios

Exercises

Try these modifications to deepen your understanding:

  1. Multi-page navigation: Navigate to 3 different websites and collect titles
  2. Form interaction: Find a form online and fill it out programmatically
  3. Network monitoring: Subscribe to network events and log all requests
  4. Multi-tab: Open 3 tabs and navigate each to a different site
  5. Error handling: Navigate to an invalid URL and handle the error gracefully

Summary

You've learned how to:

  • ✓ Set up a WebDriverBiDi.NET project
  • ✓ Connect to a browser
  • ✓ Navigate to websites
  • ✓ Execute JavaScript
  • ✓ Capture screenshots
  • ✓ Handle errors gracefully

Continue exploring the documentation to unlock more powerful automation capabilities!