browser-commander 0.10.6

Universal browser automation library that supports multiple browser engines with a unified API
Documentation

Browser Commander

A Rust library for universal browser automation that provides a unified API for different browser automation engines. The key focus is on stoppable page triggers - ensuring automation logic is properly mounted/unmounted during page navigation.

Installation

Add this to your Cargo.toml:

[dependencies]
browser-commander = "0.9"
tokio = { version = "1.0", features = ["full"] }

Core Concept: Page State Machine

Browser Commander manages the browser as a state machine with two states:

+------------------+                      +------------------+
|                  |   navigation start   |                  |
|  WORKING STATE   | -------------------> |  LOADING STATE   |
|  (action runs)   |                      |  (wait only)     |
|                  |   <-----------------  |                  |
+------------------+     page ready       +------------------+

LOADING STATE: Page is loading. Only waiting/tracking operations are allowed. No automation logic runs.

WORKING STATE: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM.

Quick Start

use browser_commander::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Launch a browser with chromiumoxide engine
    let options = LaunchOptions::chromiumoxide().headless(true);
    let result = launch_browser(options).await?;
    println!("Browser launched: {:?}", result.browser.engine);

    // `result.page` is an `Arc<dyn EngineAdapter>` you can pass to any
    // of the navigation / interaction helpers.
    let page = result.page.as_ref();

    // Navigate to a URL
    page.goto("https://example.com").await?;

    // Click a button
    page.click("button.submit").await?;

    // Fill a text field
    page.fill("input[name='email']", "test@example.com").await?;

    Ok(())
}

Features

  • Unified API across multiple browser engines
  • Native Rust Chromiumoxide support
  • Playwright and Puppeteer support through a Node.js bridge
  • Built-in navigation safety handling
  • Element visibility and scroll management
  • Click, fill, and other interaction support with verification
  • Async/await support with Tokio

API Reference

Browser Launch

use browser_commander::prelude::*;

// Launch with chromiumoxide (CDP-based)
let options = LaunchOptions::chromiumoxide()
    .headless(true)
    .user_data_dir("~/.browser-data");

let result = launch_browser(options).await?;

Playwright and Puppeteer

Rust does not have official Playwright or Puppeteer bindings. To keep the same engine names available from Rust, Browser Commander starts a local Node.js bridge and delegates operations to the official Node packages.

Install the package you want Node to resolve:

npm install playwright
npm install puppeteer

Then configure the bridge working directory if the packages are not installed from the process current directory:

use browser_commander::prelude::*;

let playwright = LaunchOptions::playwright()
    .headless(true)
    .node_working_dir("./js");

let puppeteer = LaunchOptions::puppeteer()
    .headless(true)
    .node_working_dir("./js");

Reuse a system-installed Chrome-family browser by selecting its channel or providing an explicit executable path. channel applies to the Playwright and Puppeteer bridge engines; executable_path also applies to Chromiumoxide:

let playwright = LaunchOptions::playwright()
    .channel("chrome")
    .headless(true)
    .node_working_dir("./js");

let chromiumoxide = LaunchOptions::chromiumoxide()
    .executable_path("/usr/bin/google-chrome")
    .headless(true);

You can also set a custom Node executable:

let options = LaunchOptions::playwright()
    .node_executable("/usr/local/bin/node")
    .node_working_dir("./js");

LaunchOptions::fantoccini() is still accepted for source compatibility, but launch_browser() does not yet start a managed WebDriver process.

Connect to a Running Browser over CDP

connect_browser() attaches to an externally managed Chrome-family browser and returns the same LaunchResult page adapter as launch_browser(). Use Chromiumoxide natively, or the Playwright/Puppeteer Node.js bridges:

use browser_commander::prelude::*;

let native = connect_browser(
    ConnectOptions::chromiumoxide()
        .cdp_endpoint("http://127.0.0.1:9222"),
).await?;

let playwright = connect_browser(
    ConnectOptions::playwright()
        .ws_endpoint("ws://127.0.0.1:9222/devtools/browser/<id>")
        .node_working_dir("./js"),
).await?;

native.page.goto("https://example.com").await?;

Exactly one endpoint is required. When starting Chrome 136 or newer yourself, pass a non-default --user-data-dir together with the remote-debugging flag; Chrome intentionally disables remote debugging for its default data directory. Cookies can be supplied explicitly with ConnectOptions::seed_cookies().

Launch and Connect to an Installed Browser

launch_real_browser() discovers and starts genuine installed Chrome, Edge, Brave, or Chromium with a dedicated profile, waits for its loopback CDP endpoint, and attaches with Chromiumoxide or the Playwright/Puppeteer bridges:

use browser_commander::prelude::*;
use serde_json::json;

let result = launch_real_browser(
    RealBrowserOptions::playwright()
        .channel("chrome")
        .user_data_dir("/tmp/browser-commander-profile")
        .seed_cookies(vec![json!({
            "name": "session",
            "value": "saved",
            "url": "https://example.com"
        })])
        .node_working_dir("./js"),
).await?;

result.page.goto("https://example.com").await?;
println!("CDP endpoint: {}", result.cdp_endpoint);

An explicit executable_path can replace channel discovery. Known default profiles and custom arguments that override the loopback address, debugging port, or profile are rejected. RealBrowserLaunchResult owns a browser_process handle and terminates the spawned browser when dropped. launch_and_connect_real_browser() is an alias.

Navigation

// Navigate to URL
goto(&page, "https://example.com", None).await?;

// Navigate with options
let nav_options = NavigationOptions {
    wait_until: WaitUntil::NetworkIdle,
    timeout: Some(30000),
};
goto(&page, "https://example.com", Some(nav_options)).await?;

// Wait for URL to match condition
wait_for_url_condition(&page, |url| url.contains("success")).await?;

Element Interactions

// Click a button
click_button(&page, "button.submit", None).await?;

// Click with options
let click_options = ClickOptions {
    scroll_into_view: true,
    wait_for_navigation: true,
    ..Default::default()
};
click_button(&page, "button.submit", Some(click_options)).await?;

// Fill text area
fill_text_area(&page, "textarea.message", "Hello world", None).await?;

// Scroll element into view
scroll_into_view(&page, ".target-element", None).await?;

// Keyboard interactions
press_key(&engine, "Escape").await?;
press_key(&engine, "Enter").await?;
type_text(&engine, "Hello World").await?;
key_down(&engine, "Control").await?;
key_up(&engine, "Control").await?;

Element Queries

// Check visibility
let visible = is_visible(&page, ".element").await?;

// Check if enabled
let enabled = is_enabled(&page, "button.submit").await?;

// Get text content
let text = text_content(&page, ".message").await?;

// Get attribute value
let href = get_attribute(&page, "a.link", "href").await?;

// Count matching elements
let count = count(&page, ".item").await?;

Utilities

// Wait for a duration
wait(1000).await;

// Get current URL
let url = get_url(&page).await?;

// Parse URL
let parsed = parse_url("https://example.com/path?query=value")?;

// Evaluate JavaScript
let result: String = evaluate(&page, "document.title").await?;

Modules

  • core - Core types and traits (constants, engine adapter, logger)
  • elements - Element operations (selectors, visibility, content)
  • interactions - User interactions (click, scroll, fill, keyboard)
  • browser - Browser management (launcher, navigation)
  • utilities - General utilities (URL handling, wait operations)
  • high_level - High-level DRY utilities

Prelude

For convenience, import everything commonly needed with:

use browser_commander::prelude::*;

Extensibility / Escape Hatch

browser-commander cannot anticipate every browser API. When you need an API that is not yet supported, you can access the raw underlying engine objects directly as an official extensibility escape hatch.

Using LaunchResult raw fields

launch_browser() returns a LaunchResult with a browser field that contains the engine type and configuration. When using the underlying engine crate (e.g. chromiumoxide) directly, the raw browser and page objects are accessible from the engine crate:

use browser_commander::prelude::*;

let options = LaunchOptions::chromiumoxide().headless(true);
let result = launch_browser(options).await?;

// Access engine metadata
println!("Engine: {:?}", result.browser.engine);
println!("User data dir: {:?}", result.browser.user_data_dir);

// For engine-specific APIs not yet in browser-commander,
// use the underlying engine crate directly alongside browser-commander.
// For example, with chromiumoxide:
//   let (browser, mut handler) = Browser::launch(BrowserConfig::builder()...).await?;
//   let page = browser.new_page("about:blank").await?;
//   // Use page.pdf(), page.emulate_media(), page.keyboard() etc.

Why This Matters

  • Users can adopt browser-commander incrementally while retaining access to full engine APIs
  • No need for fragile _page private-field hacks
  • Missing APIs can be reported as issues while users remain unblocked

License

UNLICENSE