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.Terminateso 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
webSocketUrlfrom 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:
- Core Concepts - Understand modules, commands, and events
- Events and Observables - Master event handling
- Module Guides - Learn about specific modules
- 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:
- Multi-page navigation: Navigate to 3 different websites and collect titles
- Form interaction: Find a form online and fill it out programmatically
- Network monitoring: Subscribe to network events and log all requests
- Multi-tab: Open 3 tabs and navigate each to a different site
- 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!