Script Module
The Script module provides functionality for executing JavaScript in the browser, managing preload scripts, and working with JavaScript values and objects.
Overview
The Script module allows you to:
- Execute JavaScript code in browsing contexts
- Call JavaScript functions with arguments
- Add preload scripts that run before page scripts
- Manage script execution realms (sandboxes)
- Send messages from scripts back to your code
- Work with JavaScript objects and DOM elements
Accessing the Module
ScriptModule script = driver.Script;
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.
Evaluating JavaScript
Evaluate Expression
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
"document.title",
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
if (result is EvaluateResultSuccess success &&
success.Result is StringRemoteValue titleValue)
{
string title = titleValue.Value;
Console.WriteLine($"Title: {title}");
}
Evaluate with Complex Expression
// The parentheses make the braces an object literal. Without them, the expression
// is parsed as a script whose braces open a block, which is a syntax error here.
string expression = """
({
title: document.title,
url: window.location.href,
elementCount: document.querySelectorAll('*').length
})
""";
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
expression,
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
Calling Functions
Call Function with Arguments
string functionDefinition = "(a, b) => a + b";
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
functionDefinition,
new ContextTarget(contextId),
true);
parameters.Arguments.Add(LocalValue.Number(5));
parameters.Arguments.Add(LocalValue.Number(10));
EvaluateResult result = await driver.Script.CallFunctionAsync(parameters);
if (result is EvaluateResultSuccess success &&
success.Result is NumberRemoteValue sumValue)
{
long sum = sumValue;
Console.WriteLine($"Sum: {sum}"); // 15
}
Call Function with DOM Element
EvaluateResult elementResult = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
new ContextTarget(contextId),
true));
if (elementResult is EvaluateResultSuccess elementSuccess &&
elementSuccess.Result is NodeRemoteValue element)
{
string functionDefinition = "(element) => element.click()";
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
functionDefinition,
new ContextTarget(contextId),
false);
parameters.Arguments.Add(element.ToSharedReference());
await driver.Script.CallFunctionAsync(parameters);
}
Call Method on Object
// Get object reference
EvaluateResult objectResult = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.getElementById('myDiv')",
new ContextTarget(contextId),
true));
if (objectResult is EvaluateResultSuccess objectSuccess)
{
if (!objectSuccess.Result.TryAs(out NodeRemoteValue? divElement))
{
return;
}
// Call getAttribute method
string functionDefinition = "(element, attrName) => element.getAttribute(attrName)";
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
functionDefinition,
new ContextTarget(contextId),
false);
parameters.Arguments.Add(divElement.ToSharedReference());
parameters.Arguments.Add(LocalValue.String("class"));
EvaluateResult result = await driver.Script.CallFunctionAsync(parameters);
if (result is EvaluateResultSuccess success &&
success.Result is StringRemoteValue classNameValue)
{
string className = classNameValue.Value;
Console.WriteLine($"Class: {className}");
}
}
Execution Targets
Scripts can be executed in different contexts:
Context Target
Execute in a specific browsing context:
ContextTarget target = new ContextTarget(contextId);
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
"document.title",
target,
true);
Realm Target
Execute in a specific execution realm:
RealmTarget target = new RealmTarget(realmId);
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
"window.myCustomProperty",
target,
true);
Get Realms
Use GetRealmsAsync to enumerate script execution realms. Realms represent JavaScript execution contexts (window, workers, worklets). The command accepts optional GetRealmsCommandParameters to filter by browsing context and realm type. Call with null or empty parameters to get all realms:
// Get all realms (parameters optional)
GetRealmsCommandResult result = await driver.Script.GetRealmsAsync();
foreach (RealmInfo realm in result.Realms)
{
Console.WriteLine($"Realm: {realm.RealmId}, Type: {realm.Type}");
if (realm is WindowRealmInfo windowRealm)
{
Console.WriteLine($" Context: {windowRealm.BrowsingContextId}");
}
}
// Filter by browsing context
GetRealmsCommandParameters contextParams = new GetRealmsCommandParameters
{
BrowsingContextId = contextId,
};
result = await driver.Script.GetRealmsAsync(contextParams);
// Filter by realm type (e.g., window realms only)
GetRealmsCommandParameters typeParams = new GetRealmsCommandParameters
{
BrowsingContextId = contextId,
RealmType = RealmType.Window,
};
result = await driver.Script.GetRealmsAsync(typeParams);
Use the returned RealmId with RealmTarget to execute script in a specific realm. WindowRealmInfo provides BrowsingContextId and Sandbox for window realms.
Sandboxed Execution
Execute in a sandbox to isolate from page scripts:
ContextTarget target = new ContextTarget(contextId)
{
Sandbox = "myIsolatedSandbox",
};
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
"window.isolatedData = { value: 42 }",
target,
false);
await driver.Script.EvaluateAsync(parameters);
// Later, access the same sandbox
target = new ContextTarget(contextId)
{
Sandbox = "myIsolatedSandbox",
};
parameters = new EvaluateCommandParameters(
"window.isolatedData.value",
target,
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
Preload Scripts
Preload scripts run before any page scripts, allowing you to:
- Inject utilities into every page
- Monitor page behavior
- Modify page behavior before it starts
Add Preload Script
string preloadScript = """
() => {
window.myUtility = {
getElementTag: (element) => element.tagName
};
}
""";
AddPreloadScriptCommandParameters parameters = new AddPreloadScriptCommandParameters(preloadScript);
AddPreloadScriptCommandResult result = await driver.Script.AddPreloadScriptAsync(parameters);
string preloadScriptId = result.PreloadScriptId;
Preload Script with Arguments
Preload script arguments in WebDriver BiDi use channels to pass values. Use ChannelValue with ChannelProperties:
SubscribeCommandParameters subscribe =
new SubscribeCommandParameters(driver.Script.OnMessage.EventName);
await driver.Session.SubscribeAsync(subscribe);
driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
if (e.ChannelId == "myChannel")
{
Console.WriteLine($"Message from preload: {e.Data.As<StringRemoteValue>().Value}");
}
});
string preloadScript = """
(channel) => {
window.addEventListener('load', () => {
channel('Page loaded');
});
}
""";
ChannelValue channel = new ChannelValue(new ChannelProperties("myChannel"));
AddPreloadScriptCommandParameters parameters = new AddPreloadScriptCommandParameters(preloadScript);
parameters.Arguments.Add(channel);
await driver.Script.AddPreloadScriptAsync(parameters);
Sandboxed Preload Script
string preloadScript = """
() => {
window.isolatedUtils = {
getPageTitle: () => document.title
};
}
""";
AddPreloadScriptCommandParameters parameters = new AddPreloadScriptCommandParameters(preloadScript)
{
Sandbox = "utilsSandbox",
};
await driver.Script.AddPreloadScriptAsync(parameters);
// Later, call the utility from the same sandbox
ContextTarget target = new ContextTarget(contextId)
{
Sandbox = "utilsSandbox",
};
EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
"window.isolatedUtils.getPageTitle()",
target,
true);
EvaluateResult result = await driver.Script.EvaluateAsync(evalParams);
Remove Preload Script
RemovePreloadScriptCommandParameters parameters =
new RemovePreloadScriptCommandParameters(preloadScriptId);
await driver.Script.RemovePreloadScriptAsync(parameters);
Working with Remote Values
Accessing Primitive Values
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters("42", new ContextTarget(contextId), true));
if (result is EvaluateResultSuccess success)
{
RemoteValue value = success.Result;
switch (value.Type)
{
case RemoteValueType.String:
string str = value.As<StringRemoteValue>().Value;
break;
case RemoteValueType.Number:
long num = value.As<NumberRemoteValue>();
break;
case RemoteValueType.Boolean:
bool flag = value.As<BooleanRemoteValue>().Value;
break;
case RemoteValueType.Null:
case RemoteValueType.Undefined:
break;
}
}
Accessing Objects
string expression = """
({
name: 'John',
age: 30,
active: true
})
""";
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
expression,
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
if (result is EvaluateResultSuccess success &&
success.Result is KeyValuePairCollectionRemoteValue obj &&
obj.Value is RemoteValueDictionary dict)
{
// Access as RemoteValueDictionary; Value is null if the remote end omitted the contents
Console.WriteLine($"Name: {dict["name"].As<StringRemoteValue>().Value}");
Console.WriteLine($"Age: {dict["age"].As<NumberRemoteValue>().Value}");
}
Accessing Arrays
string expression = "[1, 2, 3, 4, 5]";
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
expression,
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
if (result is EvaluateResultSuccess success &&
success.Result is CollectionRemoteValue listValue &&
listValue.Value is RemoteValueList list)
{
// Access as RemoteValueList; Value is null if the remote end omitted the contents
Console.WriteLine($"Array length: {list.Count}");
foreach (RemoteValue item in list)
{
Console.WriteLine($" Item: {item.As<NumberRemoteValue>().Value}");
}
}
Working with DOM Elements
EvaluateResult result = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
new ContextTarget(contextId),
true));
if (result is EvaluateResultSuccess success)
{
RemoteValue elementRemoteValue = success.Result;
// Check if it's a node
if (elementRemoteValue.TryAs(out NodeRemoteValue? element))
{
// Get node properties; this throws if the node was sent without them
NodeProperties nodeProps = element.GetNodeProperties();
Console.WriteLine($"Tag: {nodeProps.LocalName}");
Console.WriteLine($"Node type: {nodeProps.NodeType}");
// Get attributes
if (nodeProps.Attributes != null)
{
foreach (var attr in nodeProps.Attributes)
{
Console.WriteLine($" {attr.Key} = {attr.Value}");
}
}
// Get shared ID for later use
string? sharedId = element.SharedId;
// Create reference to pass to other commands
SharedReference elementRef = element.ToSharedReference();
}
}
Disowning Handles
When you evaluate or call functions that return object references (e.g., DOM elements), the browser keeps those objects alive. Use DisownAsync to release handles when you no longer need them, allowing the script engine to garbage collect the objects. Handles are populated on returned values only when you request them via ResultOwnership.Root; pass the target (context or realm) and the handle values (from the value's Handle property — note that a node's SharedId is a different identifier and cannot be disowned):
// Request root ownership so the returned value carries a handle.
EvaluateResult evalResult = await driver.Script.EvaluateAsync(
new EvaluateCommandParameters(
"document.querySelector('button')",
new ContextTarget(contextId),
true)
{
ResultOwnership = ResultOwnership.Root,
});
if (evalResult is EvaluateResultSuccess success &&
success.Result is NodeRemoteValue element)
{
string? handle = element.Handle;
if (handle != null)
{
// Release the handle when no longer needed
DisownCommandParameters parameters = new DisownCommandParameters(
new ContextTarget(contextId),
handle);
await driver.Script.DisownAsync(parameters);
}
}
Disowning is optional but recommended when you hold many references or run long-lived automation to avoid memory growth.
Creating Local Values
When passing values to JavaScript:
Primitive Values
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
"(str, num, bool) => ({ str, num, bool })",
new ContextTarget(contextId),
true);
parameters.Arguments.Add(LocalValue.String("Hello"));
parameters.Arguments.Add(LocalValue.Number(42));
parameters.Arguments.Add(LocalValue.Boolean(true));
Special Values
parameters.Arguments.Add(LocalValue.Null);
parameters.Arguments.Add(LocalValue.Undefined);
parameters.Arguments.Add(LocalValue.Number(double.PositiveInfinity));
parameters.Arguments.Add(LocalValue.Number(double.NaN));
Objects
Dictionary<string, LocalValue> obj = new Dictionary<string, LocalValue>
{
{ "name", LocalValue.String("John") },
{ "age", LocalValue.Number(30) },
{ "active", LocalValue.Boolean(true) },
};
parameters.Arguments.Add(LocalValue.Object(obj));
Arrays
List<LocalValue> array = new List<LocalValue>
{
LocalValue.Number(1),
LocalValue.Number(2),
LocalValue.Number(3),
};
parameters.Arguments.Add(LocalValue.Array(array));
Dates
parameters.Arguments.Add(LocalValue.Date(DateTime.Now));
Regular Expressions
parameters.Arguments.Add(LocalValue.RegExp("\\d+", "g"));
Handling Script Errors
Try-Catch Pattern
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
"throw new Error('Something went wrong')",
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
if (result is EvaluateResultException exception)
{
Console.WriteLine($"Error: {exception.ExceptionDetails.Text}");
Console.WriteLine($"Line: {exception.ExceptionDetails.LineNumber}");
Console.WriteLine($"Column: {exception.ExceptionDetails.ColumnNumber}");
// StackTrace is always present; an empty CallFrames list is how "no frames" arrives.
Console.WriteLine($"Stack: {exception.ExceptionDetails.StackTrace.CallFrames.Count} frames");
}
Catching JavaScript Errors
// script.evaluate evaluates an expression, so a bare top-level `return` is a
// JavaScript SyntaxError. Wrap the try/catch in an immediately invoked function
// so that the returns belong to a function body and the whole thing is an expression.
string safeExpression = """
(() => {
try {
return dangerousOperation();
} catch (error) {
return { error: error.message };
}
})()
""";
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
safeExpression,
new ContextTarget(contextId),
true);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
// The script itself never throws, so the result is EvaluateResultSuccess whether or not
// dangerousOperation() failed; inspect the returned object for the error property.
Awaiting Promises
Automatic Promise Resolution
// Fetch API returns a promise
string expression = """
fetch('https://api.example.com/data')
.then(response => response.json())
""";
EvaluateCommandParameters parameters = new EvaluateCommandParameters(
expression,
new ContextTarget(contextId),
true // awaitPromise = true
);
EvaluateResult result = await driver.Script.EvaluateAsync(parameters);
// Result will be the resolved JSON data
Async Functions
string functionDefinition = """
async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
}
""";
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
functionDefinition,
new ContextTarget(contextId),
true // awaitPromise = true
);
EvaluateResult result = await driver.Script.CallFunctionAsync(parameters);
Events
Script Message Event
driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
Console.WriteLine($"Channel: {e.ChannelId}");
Console.WriteLine($"Data: {e.Data.As<StringRemoteValue>().Value}");
Console.WriteLine($"Source context: {e.Source.BrowsingContextId}");
});
SubscribeCommandParameters subscribe =
new SubscribeCommandParameters(driver.Script.OnMessage.EventName);
await driver.Session.SubscribeAsync(subscribe);
Realm Created/Destroyed
Common Patterns
Element Interaction Pattern
// Find element
LocateNodesCommandResult locateResult = await driver.BrowsingContext.LocateNodesAsync(
new LocateNodesCommandParameters(contextId, new CssLocator("button")));
if (!locateResult.Nodes[0].TryAs(out NodeRemoteValue? element))
{
return;
}
// Click element
CallFunctionCommandParameters clickParams = new CallFunctionCommandParameters(
"(element) => element.click()",
new ContextTarget(contextId),
false);
clickParams.Arguments.Add(element.ToSharedReference());
await driver.Script.CallFunctionAsync(clickParams);
Get Element Properties Pattern
// Get multiple properties at once
string functionDefinition = """
(element) => ({
tag: element.tagName,
text: element.textContent,
visible: element.offsetParent !== null,
enabled: !element.disabled,
value: element.value
})
""";
CallFunctionCommandParameters parameters = new CallFunctionCommandParameters(
functionDefinition,
new ContextTarget(contextId),
false);
parameters.Arguments.Add(elementReference);
EvaluateResult result = await driver.Script.CallFunctionAsync(parameters);
Wait for Condition Pattern
string preloadScript = """
(channel) => {
const checkCondition = () => {
if (document.querySelector('.loaded')) {
channel({ loaded: true });
} else {
setTimeout(checkCondition, 100);
}
};
checkCondition();
}
""";
TaskCompletionSource<bool> conditionMet = new TaskCompletionSource<bool>();
// The channel delivers through the script.message event, which must be subscribed
// once per session; without this the observer below never runs.
await driver.Session.SubscribeAsync(
new SubscribeCommandParameters(driver.Script.OnMessage.EventName));
driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
if (e.ChannelId == "conditionChannel")
{
conditionMet.SetResult(true);
}
});
ChannelValue channel = new ChannelValue(
new ChannelProperties("conditionChannel"));
AddPreloadScriptCommandParameters parameters =
new AddPreloadScriptCommandParameters(preloadScript);
parameters.Arguments.Add(channel);
await driver.Script.AddPreloadScriptAsync(parameters);
await driver.BrowsingContext.NavigateAsync(navParams);
await conditionMet.Task;
Console.WriteLine("Condition met!");
Best Practices
- Use
awaitPromise: Set totruefor async operations - Handle exceptions: Check for
EvaluateResultException - Use sandboxes: Isolate your scripts from page scripts
- Cache element references: Store
SharedReferencefor reuse - Prefer functions over eval: Use
CallFunctionAsyncfor better isolation - Remove preload scripts: Clean up when no longer needed
Next Steps
- Remote Values Guide: Deep dive into JavaScript value handling
- Browsing Context Module: Finding elements
- Preload Scripts Example: Complete examples
- API Reference: Complete API documentation
API Reference
See the API documentation for complete details on all classes and methods in the Script module.