browser-commander 0.10.9

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`:

```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

```rust
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

```rust
use browser_commander::prelude::*;

// Launch with chromiumoxide (CDP-based)
let options = LaunchOptions::chromiumoxide()
    .headless(true)
    .user_data_dir("~/.browser-data")
    .with_extra_args(vec!["--lang=en-US".to_string()])
    .ignore_default_args(vec!["--disable-infobars".to_string()]);

let result = launch_browser(options).await?;
```

Browser Commander applies the documented
[automation-friendly defaults](../docs/feature-parity.md#automation-friendly-launch-defaults),
including `--password-store=basic` to avoid an extra OS credential dialog.
`ignore_all_default_args()` omits every optional Browser Commander and engine
default; `with_args()` remains a compatible append-only builder.

### 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:

```bash
npm install playwright
npm install puppeteer
```

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

```rust
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:

```rust
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:

```rust
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:

```rust
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()`.
Because connection is attach-only, it cannot retrofit launch flags. Start the
external process with the documented defaults or use `launch_real_browser()`.

### Installed browser cookies

Discover profiles and read cookies in the same shape accepted by browser
contexts:

```rust
use browser_commander::{
    list_browser_profiles, read_browser_cookies, BrowserCookieReadOptions,
    BrowserProfileOptions,
};

let profiles = list_browser_profiles(
    BrowserProfileOptions::default().browser("chrome"),
)?;
println!("{profiles:#?}");

let cookies = read_browser_cookies(
    BrowserCookieReadOptions::new("chrome")
        .profile("Default")
        .domain_filter("example.com")
        .ttl_minutes(60.0),
)?;
# Ok::<(), anyhow::Error>(())
```

Each `BrowserCookie` contains `name`, `value`, `domain`, `path`, `expires`,
`http_only`, `secure`, and `same_site` (serialized as the Playwright-compatible
`httpOnly` and `sameSite` names). The explicit helper supports Chrome, Edge,
Brave, Chromium, and Firefox and never sends imported data anywhere.

Decrypted results and derived keys default to
`~/.browser-commander/cookie-cache/` with owner-only permissions. A process
lock and the default 60-minute TTL keep Keychain, libsecret/KWallet, or DPAPI
access to at most one read across concurrent and repeated processes. Use
`.refresh(true)` to coordinate a new read, `.cache_dir(...)` and
`.ttl_minutes(...)` to customize storage, or `.cache(false)` to opt out.

| Browser family                | macOS                                | Linux                                                                       | Windows                                       |
| ----------------------------- | ------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------- |
| Chrome, Edge, Brave, Chromium | Keychain + AES-128-CBC (`v10`/`v11`) | libsecret/KWallet + AES-128-CBC (`v11`), or the Chromium `v10` fallback key | DPAPI-protected AES-256-GCM key (`v10`/`v11`) |
| Firefox                       | `cookies.sqlite`                     | `cookies.sqlite`                                                            | `cookies.sqlite`                              |

Chromium database-version-24 domain hashes and 1601-based timestamps are
handled automatically. Current Windows Chromium may use app-bound `v20`
encryption, which requires the browser's privileged service and cannot be
decrypted by an ordinary external process. The helper reports this boundary;
use a browser-supported export or saved Browser Commander storage state for
those cookies. `.ignore_decryption_errors(true)` returns the remaining
decryptable cookies. Treat imported cookies like passwords: use a short TTL,
never commit cache files, and seed only a dedicated automation profile.

### 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:

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

let result = launch_real_browser(
    RealBrowserOptions::playwright()
        .channel("chrome")
        .user_data_dir("/tmp/browser-commander-profile")
        .with_extra_args(vec!["--lang=en-US".to_string()])
        .ignore_default_args(vec!["--disable-infobars".to_string()])
        .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.
The remote-debugging address, port, and profile remain managed; headless mode
is opt-in and uses `--headless=new`.

### Navigation

```rust
// 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

```rust
// 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

```rust
// 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

```rust
// 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:

```rust
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:

```rust
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]https://github.com/link-foundation/browser-commander/issues while users remain unblocked

## License

[UNLICENSE](../LICENSE)