Table of Contents

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

  1. Use awaitPromise: Set to true for async operations
  2. Handle exceptions: Check for EvaluateResultException
  3. Use sandboxes: Isolate your scripts from page scripts
  4. Cache element references: Store SharedReference for reuse
  5. Prefer functions over eval: Use CallFunctionAsync for better isolation
  6. Remove preload scripts: Clean up when no longer needed

Next Steps

API Reference

See the API documentation for complete details on all classes and methods in the Script module.