Table of Contents

Preload Scripts Example

This example demonstrates how to use preload scripts to inject JavaScript into pages before any other scripts execute using WebDriverBiDi.NET.

Overview

Preload scripts allow you to:

  • Inject utilities available on every page
  • Monitor page behavior before it starts
  • Modify or intercept page functionality
  • Wait for specific page conditions
  • Communicate with your test code via channels

Example 1: Basic Preload Script

string webSocketUrl = "ws://localhost:9515/session/YOUR-SESSION-ID";
BiDiDriver driver = new BiDiDriver(TimeSpan.FromSeconds(30));

try
{
    await driver.StartAsync(webSocketUrl);
    Console.WriteLine("Connected to browser");

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

    // Add a preload script that creates a utility object
    Console.WriteLine("Adding preload script...");
    string preloadScript = """
        () => {
            window.myUtils = {
                getElementText: (selector) => {
                    const element = document.querySelector(selector);
                    return element ? element.textContent : null;
                },
                clickElement: (selector) => {
                    const element = document.querySelector(selector);
                    if (element) {
                        element.click();
                        return true;
                    }
                    return false;
                }
            };
            console.log('Preload script: utilities injected');
        }
        """;

    AddPreloadScriptCommandParameters preloadParams =
        new AddPreloadScriptCommandParameters(preloadScript);

    AddPreloadScriptCommandResult preloadResult =
        await driver.Script.AddPreloadScriptAsync(preloadParams);

    Console.WriteLine($"Preload script added: {preloadResult.PreloadScriptId}");

    // Navigate - the preload script will run before page scripts
    Console.WriteLine("\nNavigating to example.com...");
    await driver.BrowsingContext.NavigateAsync(
        new NavigateCommandParameters(contextId, "https://example.com")
        { Wait = ReadinessState.Complete });

    // Use the injected utilities
    Console.WriteLine("\nUsing preload script utilities...");

    EvaluateCommandParameters evalParams = new EvaluateCommandParameters(
        "window.myUtils.getElementText('h1')",
        new ContextTarget(contextId),
        true);

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

    if (result is EvaluateResultSuccess success &&
        success.Result is StringRemoteValue textValue)
    {
        string heading = textValue.Value;
        Console.WriteLine($"Page heading: {heading}");
    }

    // Clean up
    await driver.Script.RemovePreloadScriptAsync(
        new RemovePreloadScriptCommandParameters(preloadResult.PreloadScriptId));

    Console.WriteLine("\nāœ“ Preload script example complete");
    Console.WriteLine("\nPress any key to exit...");
    Console.ReadKey();
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
finally
{
    await driver.StopAsync();
}

Example 2: Preload Script with Channel Communication

// Subscribe to script messages
SubscribeCommandParameters subscribe =
    new SubscribeCommandParameters(driver.Script.OnMessage.EventName);
await driver.Session.SubscribeAsync(subscribe);

// Set up message handler
TaskCompletionSource<string> pageLoadedSignal = new TaskCompletionSource<string>();

driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
    if (e.ChannelId == "pageLoadChannel")
    {
        Console.WriteLine($"šŸ“Ø Received message from preload script");

        if (e.Data.Type == RemoteValueType.Object &&
            e.Data is KeyValuePairCollectionRemoteValue dataRemoteValue &&
            dataRemoteValue.Value is RemoteValueDictionary data)
        {
            Console.WriteLine($"Page ready: {data["ready"].As<BooleanRemoteValue>().Value}");
            Console.WriteLine($"Load time: {data["loadTime"].As<NumberRemoteValue>().Value}ms");

            pageLoadedSignal.SetResult("complete");
        }
    }
});

// Add preload script with channel
string preloadScript = """
    (channel) => {
        const startTime = Date.now();
        
        window.addEventListener('load', () => {
            const loadTime = Date.now() - startTime;
            channel({
                ready: true,
                loadTime: loadTime,
                url: window.location.href
            });
        });
    }
    """;

ChannelValue channel = new ChannelValue(new ChannelProperties("pageLoadChannel"));

AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(preloadScript);
preloadParams.Arguments.Add(channel);

AddPreloadScriptCommandResult preloadResult =
    await driver.Script.AddPreloadScriptAsync(preloadParams);

Console.WriteLine("Preload script with channel added");

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

// Wait for signal from preload script
await pageLoadedSignal.Task;
Console.WriteLine("āœ… Page load detected by preload script");

Later examples that add OnMessage observers omit the Session.SubscribeAsync call for script.message shown in Example 2; it is required once per session. (Examples that do not use channel messaging need neither.)

Example 3: Wait for Element with Preload Script

// This preload script waits for a specific element to appear
string waitForElementScript = """
    (channel) => {
        const checkForElement = () => {
            const element = document.querySelector('.dynamic-content');
            if (element) {
                channel({
                    found: true,
                    text: element.textContent,
                    timestamp: Date.now()
                });
            } else {
                // Check again in 100ms
                setTimeout(checkForElement, 100);
            }
        };
        
        // Start checking when DOM is ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', checkForElement);
        } else {
            checkForElement();
        }
    }
    """;

TaskCompletionSource<RemoteValueDictionary> elementFoundSignal =
    new TaskCompletionSource<RemoteValueDictionary>();

driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
    if (e.ChannelId == "elementWatcher" &&
        e.Data is KeyValuePairCollectionRemoteValue dataRemoteValue &&
        dataRemoteValue.Value is RemoteValueDictionary data)
    {
        elementFoundSignal.SetResult(data);
    }
});

ChannelValue channel = new ChannelValue(new ChannelProperties("elementWatcher"));

AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(waitForElementScript);
preloadParams.Arguments.Add(channel);

await driver.Script.AddPreloadScriptAsync(preloadParams);

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

// Wait for element (with timeout)
Task<RemoteValueDictionary> elementTask = elementFoundSignal.Task;
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30));

if (await Task.WhenAny(elementTask, timeoutTask) == elementTask)
{
    RemoteValueDictionary data = await elementTask;
    Console.WriteLine($"āœ… Element found: {data["text"].As<StringRemoteValue>().Value}");
}
else
{
    Console.WriteLine("āŒ Timeout waiting for element");
}

Example 4: Sandbox Isolation

// This preload script waits for a specific element to appear
string waitForElementScript = """
    (channel) => {
        const checkForElement = () => {
            const element = document.querySelector('.dynamic-content');
            if (element) {
                channel({
                    found: true,
                    text: element.textContent,
                    timestamp: Date.now()
                });
            } else {
                // Check again in 100ms
                setTimeout(checkForElement, 100);
            }
        };
        
        // Start checking when DOM is ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', checkForElement);
        } else {
            checkForElement();
        }
    }
    """;

TaskCompletionSource<RemoteValueDictionary> elementFoundSignal =
    new TaskCompletionSource<RemoteValueDictionary>();

driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
    if (e.ChannelId == "elementWatcher" &&
        e.Data is KeyValuePairCollectionRemoteValue dataRemoteValue &&
        dataRemoteValue.Value is RemoteValueDictionary data)
    {
        elementFoundSignal.SetResult(data);
    }
});

ChannelValue channel = new ChannelValue(new ChannelProperties("elementWatcher"));

// Running the script in a named sandbox isolates it from the page's own JavaScript. It
// still sees the page's DOM, but its globals (and any it creates) are invisible to the
// page's scripts, so neither side can observe or interfere with the other.
AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(waitForElementScript)
    {
        Sandbox = "elementWatcherSandbox"
    };
preloadParams.Arguments.Add(channel);

await driver.Script.AddPreloadScriptAsync(preloadParams);

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

// Wait for element (with timeout)
Task<RemoteValueDictionary> elementTask = elementFoundSignal.Task;
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30));

if (await Task.WhenAny(elementTask, timeoutTask) == elementTask)
{
    RemoteValueDictionary data = await elementTask;
    Console.WriteLine($"āœ… Element found: {data["text"].As<StringRemoteValue>().Value}");
}
else
{
    Console.WriteLine("āŒ Timeout waiting for element");
}

// A later evaluation that must see the preload script's globals has to target the same
// sandbox; evaluated against the page instead, checkForElement is undefined.
EvaluateResult sandboxState = await driver.Script.EvaluateAsync(
    new EvaluateCommandParameters(
        "typeof checkForElement",
        new ContextTarget(contextId) { Sandbox = "elementWatcherSandbox" },
        true));

Example 5: Intercept Page Behavior

// Preload script that intercepts fetch calls
string interceptFetchScript = """
    (channel) => {
        const originalFetch = window.fetch;
        
        window.fetch = async function(...args) {
            const url = args[0];
            channel({
                type: 'fetch',
                url: url,
                timestamp: Date.now()
            });
            
            return originalFetch.apply(this, args);
        };
        
        console.log('Fetch interceptor installed');
    }
    """;

List<RemoteValueDictionary> fetchCalls = new List<RemoteValueDictionary>();

driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
    if (e.ChannelId == "fetchInterceptor" &&
        e.Data.As<KeyValuePairCollectionRemoteValue>().Value is RemoteValueDictionary data)
    {
        fetchCalls.Add(data);
        Console.WriteLine($"🌐 Fetch intercepted: {data["url"].As<StringRemoteValue>().Value}");
    }
});

ChannelValue channel = new ChannelValue(new ChannelProperties("fetchInterceptor"));

AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(interceptFetchScript);
preloadParams.Arguments.Add(channel);

await driver.Script.AddPreloadScriptAsync(preloadParams);

// Navigate to page that makes fetch calls
await driver.BrowsingContext.NavigateAsync(
    new NavigateCommandParameters(contextId, "https://jsonplaceholder.typicode.com")
    { Wait = ReadinessState.Complete });

await Task.Delay(2000);  // Wait for fetch calls

Console.WriteLine($"\nšŸ“Š Total fetch calls: {fetchCalls.Count}");

Example 6: Performance Monitoring

string performanceMonitorScript = """
    (channel) => {
        window.addEventListener('load', () => {
            const perfData = performance.getEntriesByType('navigation')[0];
            
            channel({
                domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
                loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
                domInteractive: perfData.domInteractive - perfData.fetchStart,
                totalTime: perfData.loadEventEnd - perfData.fetchStart
            });
        });
    }
    """;

RemoteValueDictionary? performanceData = null;

driver.Script.OnMessage.AddObserver((MessageEventArgs e) =>
{
    if (e.ChannelId == "performanceMonitor")
    {
        performanceData = e.Data.As<KeyValuePairCollectionRemoteValue>().Value;
    }
});

ChannelValue channel = new ChannelValue(new ChannelProperties("performanceMonitor"));

AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(performanceMonitorScript);
preloadParams.Arguments.Add(channel);

await driver.Script.AddPreloadScriptAsync(preloadParams);

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

await Task.Delay(1000);  // Wait for load event

if (performanceData != null)
{
    Console.WriteLine("\nā±ļø Performance Metrics:");
    Console.WriteLine($"  DOM Content Loaded: {performanceData["domContentLoaded"].As<NumberRemoteValue>().Value}ms");
    Console.WriteLine($"  Load Complete: {performanceData["loadComplete"].As<NumberRemoteValue>().Value}ms");
    Console.WriteLine($"  DOM Interactive: {performanceData["domInteractive"].As<NumberRemoteValue>().Value}ms");
    Console.WriteLine($"  Total Time: {performanceData["totalTime"].As<NumberRemoteValue>().Value}ms");
}

Example 7: Multiple Preload Scripts

// Script 1: Utilities
string utilitiesScript = """
    () => {
        window.testUtils = {
            highlight: (element) => {
                element.style.border = '2px solid red';
            }
        };
    }
    """;

// Script 2: Monitoring
string monitoringScript = """
    (channel) => {
        window.addEventListener('click', (e) => {
            channel({
                type: 'click',
                target: e.target.tagName,
                x: e.clientX,
                y: e.clientY
            });
        });
    }
    """;

// Add both scripts
AddPreloadScriptCommandResult utils = await driver.Script.AddPreloadScriptAsync(
    new AddPreloadScriptCommandParameters(utilitiesScript));

ChannelValue channel = new ChannelValue(new ChannelProperties("clickMonitor"));
AddPreloadScriptCommandParameters monitorParams =
    new AddPreloadScriptCommandParameters(monitoringScript);
monitorParams.Arguments.Add(channel);

AddPreloadScriptCommandResult monitor = await driver.Script.AddPreloadScriptAsync(monitorParams);

Console.WriteLine("Multiple preload scripts added");

// Both scripts will run on every navigation

Pattern: Conditional Preload Scripts

// Add preload script only for specific contexts
AddPreloadScriptCommandParameters preloadParams =
    new AddPreloadScriptCommandParameters(preloadScript);

// Limit to specific contexts
preloadParams.Contexts.Add(contextId);

AddPreloadScriptCommandResult result =
    await driver.Script.AddPreloadScriptAsync(preloadParams);

// Script will only run in the specified context

Pattern: Temporary Preload Script

// Add preload script
AddPreloadScriptCommandResult preloadResult =
    await driver.Script.AddPreloadScriptAsync(preloadParams);

try
{
    // Use preload script for several navigations
    await driver.BrowsingContext.NavigateAsync(navParams1);
    await driver.BrowsingContext.NavigateAsync(navParams2);
}
finally
{
    // Remove when done
    await driver.Script.RemovePreloadScriptAsync(
        new RemovePreloadScriptCommandParameters(preloadResult.PreloadScriptId));
}

// Future navigations won't have the preload script

Best Practices

  1. Use channels: Communicate with test code via channels
  2. Use sandboxes: Isolate preload script from page scripts
  3. Keep scripts simple: Complex logic should be in test code
  4. Remove when done: Clean up preload scripts after use
  5. Handle timing: Use load events to ensure DOM is ready
  6. Test isolation: Each test should manage its own preload scripts

Common Issues

Script Not Running

Problem: Preload script doesn't seem to execute.

Solution:

  • Add the script before navigation
  • Check for JavaScript errors in the script
  • Use console.log() in the script to verify execution

Can't Access Objects

Problem: Objects created by preload script are undefined.

Solution:

  • Ensure you're using the same sandbox
  • Check that script actually executed
  • Verify timing (DOM might not be ready)

Channel Messages Not Received

Problem: Messages from preload script aren't arriving.

Solution:

  • Subscribe to script messages before navigation
  • Use correct channel ID
  • Check that channel was passed as argument

Next Steps