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
- Use channels: Communicate with test code via channels
- Use sandboxes: Isolate preload script from page scripts
- Keep scripts simple: Complex logic should be in test code
- Remove when done: Clean up preload scripts after use
- Handle timing: Use load events to ensure DOM is ready
- 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
- Script Module: Complete script module guide
- Events and Observables: Understanding event handling
- Common Scenarios: More examples