Automate Preloaded & Sandboxed Scripts with Selenium WebDriver BiDi

Published on December 18, 2024

Java Practice for BiDi Script Domain — Preload & Sandbox

Thumbnail for article: Automate Preloaded & Sandboxed Scripts with Selenium WebDriver BiDi
Article agenda

Introduction

Browser automation has become a cornerstone of modern software testing, enabling teams to efficiently validate applications across diverse scenarios. With the rise of WebDriver BiDi (Bidirectional Protocol), testers now have advanced capabilities for interacting with browser contexts in more granular ways. Among these capabilities, preloading scripts and sandboxed scripts stand out as powerful techniques to control, monitor, and isolate test environments.

In this article, we explore how Selenium WebDriver’s BiDi APIs empower testers to preload scripts for early browser initialization and execute scripts securely within sandboxed environments. By leveraging these methods, testers can streamline debugging, enhance test control, and ensure robust isolation between test states. This guide provides practical examples, detailed explanations, and a hands-on walkthrough of implementing these techniques in Java.

Preload Script

What Does a Preloaded Script Mean?

In the context of browser test automation, preloading a script means injecting and executing a custom script into the browser environment before any user-defined scripts or the page’s own scripts are loaded and executed. This is typically done when a new tab or window is opened during automated tests.

Key Features of Preloading a Script

1. Early Execution:

  • A preloaded script runs immediately after a new browser context (e.g., tab or window) is created, before any page content or JavaScript is loaded. This ensures the script executes at the earliest possible moment.

2. Custom Initialization:

  • Preloaded scripts enable the setup of a tailored testing environment. This includes:
  • Mocking APIs
  • Injecting utility functions
  • Modifying or monitoring the DOM
  • Logging browser activity

3. Global Scope:

  • These scripts operate in the browser’s global scope, meaning they can define global variables or methods that remain accessible throughout the lifecycle of the page.

Use Cases of Preloading Scripts in Test Automation

1. Debugging and Logging:

  • Add scripts that log custom messages (e.g., console.log('Preload script executed');) to confirm that tests begin as expected or to track early browser activity.

2. API Mocking:

  • Mock network requests or supply test-specific API responses to isolate the system under test from real-world dependencies and ensure consistent results.

3. Testing Browser Behavior:

  • Modify the browser environment or capture key events, such as navigation timing and page load metrics, to assess performance and behavior.

4. Pre-defining State:

  • Inject scripts to manipulate the DOM, set cookies, simulate a logged-in user session, or load data required for the test before the page’s own scripts execute.

5. Monitoring Events:

  • Observe browser events, network activity, or console logs from the moment a new window or tab is opened, enabling deeper insights into runtime behavior.

By combining these features and use cases, preloading scripts empower testers to create highly controlled, repeatable, and insightful test scenarios, making them a powerful tool in modern test automation workflows.

How Preloading a Script Works

Let’s navigate to URL https://selenium.dev/selenium/web/blank, and preload the following script to log a message when a new browser tab/window is opened.

console.log('welcome_to_the_blank_page');
() => {{ console.log(‘{welcome_to_the_blank_page}’) }}

We can write a test method namedpreloadScriptTestto validate the functionality of a preload script using WebDriver's BiDi APIs.

preloadScriptTest method

Let's break it down step by step:

1. Adding a Preload Script

String id = script.addPreloadScript("() => {{ console.log('{welcome_to_the_blank_page}') }}");

Purpose:

  • Adds a preload script to the browser session. This script executes before any other scripts when a new page or tab is opened.

Code:

  • script.addPreloadScript(...) is a method provided by the Script module, part of Selenium’s BiDi API.
  • The script logs the message {welcome_to_the_blank_page} to the browser's console.

Return Value:

  • A unique identifier (id) is returned, which is used to reference the preload script later (e.g., for removal).

Key Aspect:

  • The preload script runs automatically when the browser navigates to a page.

2. Assertion to Validate Script Addition

Assert.assertTrue(id != null && !id.isEmpty());

Purpose:

  • Ensures that the id returned from addPreloadScript is valid.

Validation:

  • Checks that the id is not null and is not an empty string.
  • If the assertion fails, the test terminates here.

3. Inspecting Logs After Script Execution

try (LogInspector logInspector = new LogInspector(driver)) {
CompletableFuture future = new CompletableFuture<>();
logInspector.onConsoleEntry(future::complete);

driver.get(webPage);

ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
Assert.assertEquals(logEntry.getText(), "{welcome_to_the_blank_page}");
Assert.assertEquals(logEntry.getLevel(), LogLevel.INFO);
}

Step-by-Step Breakdown:

Create LogInspector:

  • LogInspector logInspector = new LogInspector(driver) initializes a log listener for the browser's console.
  • It captures all console logs during the browser session.

Setup CompletableFuture:

  • CompletableFuture future acts as a placeholder for the next console log entry.
  • The method logInspector.onConsoleEntry(future::complete) listens for the next log entry and completes the future object when a new entry appears.

Navigate to the Web Page:

  • driver.get(webPage) loads the specified URL.
  • The preload script automatically executes during page load, logging the message {welcome_to_the_blank_page}.

Capture the Console Log:

  • future.get(5, TimeUnit.SECONDS) waits for a maximum of 5 seconds to capture a console log entry.
  • The captured log is stored in ConsoleLogEntry logEntry.

Assertions:

  • Assert.assertEquals(logEntry.getText(), "{welcome_to_the_blank_page}"): Verifies that the log message matches the expected value.
  • Assert.assertEquals(logEntry.getLevel(), LogLevel.INFO): Checks that the log level is INFO.

Automatic Resource Management:

  • The try block uses a try-with-resources statement to ensure that the LogInspector instance is properly closed after use.

4. Removing the Preload Script

script.removePreloadScript(id);

Purpose: Removes the previously added preload script using the unique identifier (id).

Result: The script will no longer execute when navigating to a new page.

5. Validate Absence of Preload Script

try (LogInspector logInspector = new LogInspector(driver)) {
CompletableFuture future = new CompletableFuture<>();
logInspector.onConsoleEntry(future::complete);

driver.get(webPage);

try {
ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
Assert.assertTrue(e instanceof TimeoutException);

}
}

Step-by-Step Breakdown:

Reinitialize LogInspector:

  • Sets up a new LogInspector to listen for console logs after removing the preload script.

Navigate to the Web Page:

  • Navigates to the same web page (webPage).

Attempt to Capture a Log:

  • future.get(5, TimeUnit.SECONDS) attempts to fetch a log entry within 5 seconds.

TimeoutException Handling:

  • Since the preload script was removed, the log message {welcome_to_the_blank_page} is not generated.
  • The TimeoutException is caught, confirming the absence of the expected console log.

Assertion:

  • Assert.assertTrue(e instanceof TimeoutException) ensures the exception is of the correct type.

Why Is Preloading a Script Useful?

  • Test Setup: Ensures the browser environment is configured before any user scripts or the page itself runs.
  • Debugging: Helps identify potential issues before user scripts execute.
  • Controlled Testing: Allows fine-tuned control over browser behavior, isolating test scenarios from external variables.
  • Efficiency: Saves time by avoiding repeated setup code in each test.

Preloading a script is like setting the stage for a play — it ensures everything is ready before the main act begins.

Sandbox Scripts

What Is a Sandbox in the Context of Browser Automation?

A sandbox in the context of browser automation refers to a security mechanism that isolates the execution of processes or scripts to prevent them from affecting the broader system or accessing sensitive data. It’s like a controlled environment where potentially untrusted code or operations can run with restricted permissions.

Key Features of a Sandbox in Browser Automation

  1. Isolation
    A sandbox ensures that browser processes or test scripts don’t interfere with other applications, files, or processes on the host machine. Each process operates independently within its secure boundary.
  2. Restricted Access
    Scripts running in a sandbox cannot access critical system resources, such as files, network configurations, or hardware, beyond what is explicitly allowed.
  3. Error Containment
    If a script or automation tool encounters a bug or malicious code, the impact is contained within the sandbox, preventing harm to the host system.
  4. Simulated Environments
    Sandboxes can simulate various user environments or browser states, enabling robust testing of web applications without risking real-world consequences.

Use Cases in Browser Automation

  1. Testing Untrusted Code
    Running unverified or third-party JavaScript in a sandbox ensures that malicious actions, like unauthorized data access or system alterations, are prevented.
  2. Web Application Testing
    Automation tools like Selenium or Puppeteer often leverage sandboxes to safely execute test scripts and interact with web pages.
  3. Security Testing
    Sandboxes enable ethical hackers and testers to evaluate vulnerabilities in a web application without exposing sensitive system information.
  4. Preventing Cross-Site Attacks
    In scenarios where browser automation interacts with multiple domains, a sandbox prevents cross-site scripting (XSS) or request forgery (CSRF) attacks from impacting the broader system.

Example: Chromium’s Sandbox

The Chromium browser (used by Chrome and Edge) employs a robust sandboxing system. Each tab or extension operates in a separate process, isolated from the system and other processes. This architecture minimizes the risks of malicious sites or scripts affecting the user’s device.

How a Sandbox Script Works

Let’s type up a test method called canCallFunctionInASandbox to explore the BiDi Script domain in detail.

Here’s a step-by-step explanation and analysis of the canCallFunctionInASandbox method:

1. Retrieve the Window Handle

String id = driver.getWindowHandle();
  • Purpose: This retrieves the unique identifier (id) of the current browser window or tab.
  • Significance: This ID is used to specify the browser context where the script will execute.

2. Execute a Script Without a Sandbox

script.callFunctionInBrowsingContext(
id,
"() => { window.foo = 1; }",
true,
Optional.empty(),
Optional.empty(),
Optional.empty()
);

Purpose:

  • Executes a JavaScript function in the main browsing context (without a sandbox).
  • The script sets the global variable window.foo to 1.

Parameters:

  • id: The ID of the browsing context where the script will execute.
  • "() => { window.foo = 1; }": The JavaScript function to execute.
  • true: Indicates the script should be evaluated asynchronously.
  • Optional.empty(): Indicates no additional arguments or execution context is provided.

3. Verify That the Script Didn’t Affect the Sandbox

EvaluateResult resultNotInSandbox = script.callFunctionInBrowsingContext(
id,
"sandbox",
"() => window.foo",
true,
Optional.empty(),
Optional.empty(),
Optional.empty()
);

Purpose:

  • Executes a JavaScript function in the sandbox to retrieve the value of window.foo.

Sandbox Context:

  • "sandbox" specifies that the function runs inside an isolated environment, separate from the main browsing context.
  • Any global changes made in the main context (like setting window.foo = 1) will not affect the sandbox.

Expected Behavior:

4. Validate the Results in the Sandbox

Assert.assertEquals(resultNotInSandbox.getResultType(), EvaluateResult.Type.SUCCESS);
EvaluateResultSuccess result = (EvaluateResultSuccess) resultNotInSandbox;
Assert.assertEquals(result.getResult().getType(), "undefined");

Purpose: Verifies that:

  1. The script executed successfully in the sandbox (resultNotInSandbox.getResultType() is SUCCESS).
  2. The value of window.foo in the sandbox is "undefined".

5. Modify window.foo Inside the Sandbox

script.callFunctionInBrowsingContext(
id,
"sandbox",
"() => { window.foo = 2; }",
true,
Optional.empty(),
Optional.empty(),
Optional.empty()
);
  • Purpose: Executes a JavaScript function in the sandbox to set window.foo to 2.
  • Sandbox Isolation: This change is confined to the sandbox environment and does not affect the main browsing context or other sandboxes.

6. Verify the Changes in the Sandbox

EvaluateResult resultInSandbox = script.callFunctionInBrowsingContext(
id,
"sandbox",
"() => window.foo",
true,
Optional.empty(),
Optional.empty(),
Optional.empty()
);
  • Purpose: Executes a JavaScript function in the sandbox to retrieve the updated value of window.foo.
  • Expected Behavior: The value of window.foo should now be 2 in the sandbox.

7. Assert the Results in the Sandbox

Assert.assertEquals(resultInSandbox.getResultType(), EvaluateResult.Type.SUCCESS);
Assert.assertTrue(resultInSandbox.getRealmId() != null);
EvaluateResultSuccess resultInSandboxSuccess = (EvaluateResultSuccess) resultInSandbox;
Assert.assertEquals((Long) resultInSandboxSuccess.getResult().getValue().get(), 2L);

Purpose: Confirms:

  1. The script executed successfully in the sandbox.
  2. The sandbox’s RealmId (unique identifier for the sandbox) is valid (!= null).
  3. The value of window.foo in the sandbox is correctly updated to 2.

Summary

Core Idea: The test demonstrates how scripts can be executed in both the main browsing context and an isolated sandbox.

Key Insights:

  1. Changes made in the main context do not affect the sandbox.
  2. Modifications inside the sandbox are isolated and do not impact the main context or other sandboxes.
  3. The test validates this isolation by checking the value of window.foo in both contexts before and after updates.

Importance in Automation Testing

This approach is crucial for scenarios where isolation of state and behavior is necessary, such as secure testing, debugging, and multi-context automation.

  • Ensures test environments mimic real-world constraints safely.
  • Protects the test machine from potentially dangerous test scenarios.
  • Facilitates debugging by isolating issues within the sandbox.

In summary, a sandbox provides a controlled, secure environment critical for safe and effective browser automation, especially when dealing with untrusted scripts or testing web application vulnerabilities.

Full Code & Test Execution

Below captioned is the complete code implementing our two test cases for the BiDi Script module.

ScriptTest.java:

package BiDiScriptDomain;

import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.log.ConsoleLogEntry;
import org.openqa.selenium.bidi.log.LogLevel;
import org.openqa.selenium.bidi.module.LogInspector;
import org.openqa.selenium.bidi.module.Script;
import org.openqa.selenium.bidi.script.EvaluateResult;
import org.openqa.selenium.bidi.script.EvaluateResultSuccess;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import org.testng.Assert;

public class ScriptTest {

private WebDriver driver;
private LogInspector logInspector;
private Script script;

private String webPage = "https://selenium.dev/selenium/web/blank";

@BeforeTest
public void setup() {
FirefoxOptions options = new FirefoxOptions();
options.enableBiDi();
driver = new FirefoxDriver(options);
logInspector = new LogInspector(driver);
script = new Script(driver);
}

@AfterTest
public void teardown() {
logInspector.close();
script.close();
driver.quit();
}

@Test
public void preloadScriptTest() throws InterruptedException, TimeoutException, ExecutionException {

String id = script.addPreloadScript("() => {{ console.log('{welcome_to_the_blank_page}') }}");

Assert.assertTrue(id != null && !id.isEmpty());

try (LogInspector logInspector = new LogInspector(driver)) {
CompletableFuture future = new CompletableFuture<>();
logInspector.onConsoleEntry(future::complete);

driver.get(webPage);

ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);

Assert.assertEquals(logEntry.getText(), "{welcome_to_the_blank_page}");
Assert.assertEquals(logEntry.getLevel(), LogLevel.INFO);
}

script.removePreloadScript(id);

try (LogInspector logInspector = new LogInspector(driver)) {
CompletableFuture future = new CompletableFuture<>();
logInspector.onConsoleEntry(future::complete);

driver.get(webPage);

try {
ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
Assert.assertTrue(e instanceof TimeoutException);

}
}

}

@Test
public void canCallFunctionInASandbox() {
String id = driver.getWindowHandle();

// Make changes without sandbox
script.callFunctionInBrowsingContext(id, "() => { window.foo = 1; }", true, Optional.empty(), Optional.empty(),
Optional.empty());

// Check changes are not present in sandbox
EvaluateResult resultNotInSandbox = script.callFunctionInBrowsingContext(id, "sandbox", "() => window.foo",
true, Optional.empty(), Optional.empty(), Optional.empty());

Assert.assertEquals(resultNotInSandbox.getResultType(), EvaluateResult.Type.SUCCESS);

EvaluateResultSuccess result = (EvaluateResultSuccess) resultNotInSandbox;
Assert.assertEquals(result.getResult().getType(), "undefined");

// Make changes in sandbox
script.callFunctionInBrowsingContext(id, "sandbox", "() => { window.foo = 2; }", true, Optional.empty(),
Optional.empty(), Optional.empty());

// check if changes are present in sandbox
EvaluateResult resultInSandbox = script.callFunctionInBrowsingContext(id, "sandbox", "() => window.foo", true,
Optional.empty(), Optional.empty(), Optional.empty());

Assert.assertEquals(resultInSandbox.getResultType(), EvaluateResult.Type.SUCCESS);
Assert.assertTrue(resultInSandbox.getRealmId() != null);

EvaluateResultSuccess resultInSandboxSuccess = (EvaluateResultSuccess) resultInSandbox;

Assert.assertEquals((Long) resultInSandboxSuccess.getResult().getValue().get(), 2L);

}

}

When we run the program, both tests pass.

Test Execution: https://youtu.be/EdqGre7UY7k

Conclusion

Selenium WebDriver BiDi unlocks unprecedented opportunities for fine-grained control in browser automation. Preloading scripts ensures that custom logic runs at the earliest possible moment, establishing a foundation for debugging, API mocking, and event monitoring. Meanwhile, sandboxed scripts provide a safe, isolated playground to test untrusted code or simulate constrained environments without risking system integrity.

By combining these methods, testers can achieve unparalleled precision and security in their automation workflows. The examples provided in this guide demonstrate how these features work in harmony, ensuring efficient test setups and reliable outcomes. As browser automation continues to evolve, embracing these advanced BiDi functionalities will undoubtedly elevate your testing strategies to new heights.

𝓗𝒶𝓅𝓅𝓎 𝓉𝓮𝓈𝓉𝒾𝓃𝓰 𝒶𝓃𝒹 𝒹𝓮𝒷𝓊𝓰𝓰𝒾𝓃𝓰!

I welcome any comments and contributions to the subject. Connect with me on LinkedIn, X , GitHub, or Insta. Check out my website.

If you find this post useful, please consider buying me a coffee.