Skip to main content

This guide is for anyone who wants to understand, extend, or maintain the Rust port of SeleniumBase (seleniumbase-rs). It covers the repository layout, the BaseCase architecture, how to add new helpers, the stealth system, testing, and release considerations.

Info

What you will learn

  • How the repository is organized.
  • How BaseCase methods are split across domain files.
  • How to add a new BaseCase helper or evasion provider.
  • How to run tests and interpret CI checks.
  • Where to find the public API contract and stability notes.

Project goals#

seleniumbase-rs aims to provide a Rust-native browser-automation and end-to-end testing framework with parity to the Python SeleniumBase API. The public surface is centered around BaseCase, a stateful test object that manages a browser session, configuration, recorder, deferred assertions, tours, charts, presentations, and optional stealth/CDP integrations.

Repository layout#

seleniumbase-rs
Cargo.tomlCrate manifest (optional cloud/Playwright/MCP features)
README.mdUser-facing quick-start and feature index
DOCS.mdOverview of major features
docsTutorials and help pages
tutorialsStep-by-step guides
DEVELOPER_GUIDE.mdThis file
examplesRunnable example programs
src
lib.rsCrate root, public re-exports
apiPublic test API; more modules than the ones listed
base_case.rsBaseCase struct and core inherent impl
base_case_implsPer-domain BaseCase method modules
chart.rsChart generation
tour.rsGuided tour builder
presentation.rsHTML presentation builder
deferred.rsDeferred assertion state
recorder.rsAction recorder
html.rsBeautifulSoup-style HTML parsing
pdf.rsPDF helpers
runner.rsAsync browser test lifecycle
traits.rsCapability traits (BrowserApi, ElementApi, ...)
sb_cdpPure CDP engine: Browser, Page, Locator, input, state, CAPTCHA
mcpMCP servers: cdp, driver and sb toolsets
browserBrowser launch, session, and configuration
config.rsBrowserConfig, Browser, DriverMode
session.rsBrowserSession (WebDriver wrapper)
launcher.rsWebDriver/chromedriver launching
playwright.rsOptional Playwright-backed session
stealthStealth / undetected automation helpers
dprocess.rsDetached browser/driver process management
options.rsStealthOptions builder
reactor.rsCDP Fetch interceptor
behaveGherkin/BDD runner and step registry
cliCommand-line tooling: the library half of the sbrs binary
commonEncryption, obfuscation, graceful shutdown
configSettings and proxy/ad-block lists
coreTest result records, JSON/HTML report writers
js_codequote: safe JavaScript string literals for built scripts
pluginsCloud/logging plugin interfaces
utilitiesSelenium IDE, Grid, and Python migration
utilsSelectors, shadow DOM, translations, extensions
crates
cliBinary targets: sbrs and the MCP server sbrs-mcp (package seleniumbase-rs-cli)
parityUpstream API manifest (see docs/UPSTREAM_SYNC.md)
tools
parityScripts that extract and seed the manifest
testsIntegration tests

BaseCase architecture#

BaseCase is a single struct that owns the test state:

src/api/base_case.rs

The inherent implementation is split across src/api/base_case.rs (core constructors and commonly used methods) and src/api/base_case_impls/*.rs (domain-specific helpers). Each included file is an impl BaseCase { ... } block; they rely on the imports declared at the top of base_case.rs.

Domain files:

src/api/base_case_impls
base_case_impl_common.rsconstructors, lifecycle, and widely-shared helpers
base_case_impl_alerts.rsalert and prompt handling
base_case_impl_browser.rsbrowser introspection and state
base_case_impl_charts.rschart generation helpers
base_case_impl_dom.rsDOM querying and manipulation
base_case_impl_downloads.rsfile download helpers
base_case_impl_links.rslink and anchor helpers
base_case_impl_storage.rscookies and web-storage helpers
base_case_impl_window.rswindows, frames, and tabs
base_case_impl_mouse.rsmouse and hover actions
base_case_impl_nav.rsnavigation and URL helpers
base_case_impl_media.rsmedia and screenshot helpers
base_case_impl_tours.rsguided tour helpers
base_case_impl_presentations.rspresentation helpers
base_case_impl_jslibs.rsthird-party JS library injection
base_case_impl_misc.rstest-control and message-overload helpers
base_case_impl_pdf_html.rsPDF and HTML parsing helpers
base_case_impl_extra.rsextra assertion and wait helpers
base_case_impl_cdp_page.rsCDP-page helpers
base_case_impl_dialog_inspector.rsdialog inspection helpers
base_case_impl_shadow.rsshadow-DOM helpers
base_case_impl_gui.rsGUI automation helpers; compiled only with the gui feature, so lint and test it with --features gui
base_case_impl_masterqa.rsMasterQA helpers

Adding a new BaseCase helper#

Choose the domain file

Choose the appropriate domain file in src/api/base_case_impls/ (or create a new one if the domain does not exist).

Add the method

Add the method inside the existing impl BaseCase { ... } block.

Return a Result

Return crate::Result<T> (an alias for Result<T, SeleniumBaseError>).

Record the action

Record the action with self.record(...) when it represents a user-facing test step.

Test it

Add a unit test when the helper does not require a live browser. A test that needs a BaseCase with a WebDriver session can start a MockWebDriver (in base_case_impl_common.rs): it answers the WebDriver commands you choose and records what the BaseCase sent.

Lint and run

Run cargo clippy --all-targets -- -D warnings and cargo test.

Example:

Example helper

Capability traits#

The public API is also exposed through capability traits in src/api/traits.rs:

  • BrowserApi - navigation and lifecycle
  • ElementApi - element finding and interaction
  • AssertionApi - test assertions
  • ScreenshotApi - screenshot capture

These traits are implemented for BaseCase. Prefer them when writing helpers that should work with any type exposing the same capability, and use them as a blueprint when adding new cross-cutting concerns.

Error handling#

All fallible operations return crate::Result<T>, defined as:

The Result alias

SeleniumBaseError is a thiserror enum covering WebDriver errors, I/O, JSON, invalid configuration, assertions, CDP driver failures, GUI input, and Playwright errors. Use ? to propagate errors and add context in user-facing messages.

Error taxonomy#

CategoryExample variantsTypical cause
ElementElementNotFound, ElementNotVisible, ElementNotInteractableSelector did not match or element is hidden.
DriverDriverNotReady, DriverCrashed, SessionNotCreatedchromedriver is missing, incompatible, or crashed.
ConfigInvalidConfig, UnsupportedBrowser, InvalidUrlBad BrowserConfig or profile payload.
I/OIoError, DownloadFailed, FileNotFoundFilesystem or network transfer failure.
AssertAssertionFailed, DeferredAssertFailed, SkippedTest assertion mismatch or explicit skip.
CDPCdpError, CdpMethodFailedChrome DevTools Protocol call failed.
GUIGuiInputError, OcrErrorDesktop automation or image parsing failure.
PlaywrightPlaywrightErrorPlaywright runtime error.

Helper constructors#

Prefer constructors over manual formatting:

Helper constructors

Runtime diagnostics#

Errors can emit tracing::error! events:

Runtime diagnostics

The error-backtrace feature is declared but no code reads it yet, so it adds no backtrace to SeleniumBaseError. For the backtrace of a panic, set RUST_BACKTRACE=1.

Feature flags#

The crate uses Cargo features to keep heavy or optional dependencies contained:

FeatureDefaultPurpose
tuioffTerminal apps: sbrs commander, sbrs caseplans, sbrs behave-gui. Pulls ratatui/crossterm.
guioffNative OS dialogs and GUI automation. Pulls rfd/enigo; on Linux it needs GTK and X11 development packages.
playwrightoffEnables the rustwright-based Playwright-compatible engine.
s3offEnables AWS S3 artifact uploads.
azureoffEnables Azure Blob Storage artifact uploads.
gcpoffPlaceholder for Google Cloud integrations.
mcp-serveroffBuilds the sbrs-mcp binary using rmcp.
full-tracingoffVerbose tracing span events for debugging.
error-backtraceoffDeclared for backtraces on SeleniumBaseError; no code reads it yet.

JSON logs need no feature: set SB_LOG_FORMAT=json or call init_tracing_json.

No feature of the library is on by default, so a library user or a headless pool pulls in neither the terminal UI nor the desktop automation stacks (nor any command-line crate: sbrs is the workspace package seleniumbase-rs-cli, in crates/cli, which forwards tui, gui and turso). Turn them on when you want them:

Note

The crate compiles cleanly with no features and with --all-features; keep both green when touching feature-gated code. CI also builds gui,tui on Linux and macOS, because the default build no longer covers them.

When adding a feature-gated API, use #[cfg(feature = "...")] and declare the dependency as optional = true in Cargo.toml. Prefer #[cfg(feature = "...")] over #[cfg(not(feature = "..."))] so the default build is the simpler path.

Module deep dive#

src/api/#

The public test API. base_case.rs defines the struct and core constructors; base_case_impls/*.rs split hundreds of helpers by domain. New helpers should land in the smallest domain file first, then be surfaced through BaseCase or a capability trait.

src/browser/#

Launch and session plumbing:

  • config.rs — serializable BrowserConfig, Browser enum, DriverMode.
  • session.rs — BrowserSession, the WebDriver wrapper and CDP session cache.
  • launcher.rs — chromedriver discovery, startup, and StealthOptions application.
  • playwright.rs — optional Playwright-backed session adapter.

src/stealth/#

Anti-detection layer:

  • fingerprint.rs — Fingerprint, FingerprintBuilder, Dimension, presets (including edge_desktop), coherence validation, and BrowserType; spoof.rs (Spoof<T>), settings.rs (the value types such as ScreenSize and WebGl) and policy.rs (StealthPolicy).
  • evasions.rs / providers/ — JavaScript evasion registry and 25 built-in providers.
  • patcher.rs — ChromedriverPatcher and EnginePatch definitions.
  • options.rs — StealthOptions builder for launch args and prefs.
  • reactor.rs — background CDP Fetch interceptor.
  • dprocess.rs — detached chromedriver/browser process helpers.
  • humanize.rs — Bézier mouse paths and keystroke timing, and the seedable Rng the rest of the layer shares.
  • randomize.rs — the tables and assembly behind Fingerprint::randomized (private; the method is the API).

src/profile_payloads/#

Parses external anti-detect profile JSON into BrowserConfig + Fingerprint. The mapping is explicit: every JSON field is declared in ProfileParams, then converted in ProfileParams::to_browser_config() and browser().

src/cli/#

The library half of the command line: commands.rs, devtools.rs, prompt.rs, scaffold.rs and the scripts/ that the binaries, the MCP server and the golden tests share. It has no command-line parser.

crates/cli/#

The workspace package seleniumbase-rs-cli, which builds the two binaries and owns the dependencies only they use (the usage command-line parser):

  • src/sbrs.rs — the sbrs CLI command tree, declared with the usage derives (and src/report.rs, sbrs report). The usage spec completions/sbrs.usage.kdl and the page docs/help/cli_reference.md are generated from it by a test.
  • src/fig.rs — the Fig spec of sbrs completions fig, written from the same tables.
  • crates/cli/src/mcp_server.rs — sbrs-mcp, which starts the Model Context Protocol server of src/mcp/ (the tools are ToolDefs there).
  • tests/ — every test that runs the real sbrs or sbrs-mcp binary.

src/utilities/#

Migration and IDE tooling: Python importer, Selenium IDE parser, Selenium Grid helpers.

Browser session model#

BrowserSession wraps a thirtyfour::WebDriver and exposes higher-level methods such as wait_for_element, text, click, and execute_script. Most BaseCase helpers delegate to the session. For CDP-only operations (such as activating a stealth driver), CdpDriver and CdpPage provide a lightweight Chrome DevTools Protocol client.

Stealth architecture#

The stealth system has four independent axes:

  1. Launch configuration — StealthOptions emits Chromium args and prefs.
  2. Runtime JavaScript — EvasionProviders generate a bootstrap script.
  3. CDP-level overrides — evasions::safe_cdp_overrides() sends Network.setUserAgentOverride, Emulation.setDeviceMetricsOverride, Emulation.setTimezoneOverride, Emulation.setLocaleOverride, Emulation.setGeolocationOverride, and related commands. When StealthPolicy::native_spoofing is enabled, providers skip JS patches for dimensions the browser can spoof natively, making the spoof invisible to page-side toString() / descriptor inspection.
  4. Network interception — StealthReactor uses CDP Fetch to override headers and responses.
  5. Binary patching — ChromedriverPatcher edits driver markers.

Use these through BaseCase::activate_cdp_mode(url) or by setting DriverMode::Uc in BrowserConfig.

Fingerprint, settings and policy#

Fingerprint is a value type describing the desired browser surface. It is built once with Fingerprint::builder(), has private fields and read accessors, and is changed only through to_builder(). It is composed of:

  • browser_type, os_type, core_version, seed — the persona.
  • one Spoof<T> per setting: user_agent, platform, hardware_concurrency, screen, webgl, geolocation, timezone, locale, webrtc, media_devices, battery, client_hints, proxy, and the rest of what a page can probe.
  • a StealthPolicy — how the claims are delivered (WebRTC IP policy, canvas and audio noise, native spoofing, granted permissions, blocked ports and trackers).

A Spoof<T> is Natural (the browser’s own value, the default), Random (a coherent value drawn when the fingerprint is built), Value(T) (a plain value converts to it) or Off (the API is unavailable or denied; only for geolocation, WebRTC, media devices, battery, Bluetooth, speech voices, network information and Client Hints). A built fingerprint holds no Random.

Providers, launch arguments and protocol overrides do not read the fields by themselves: they ask Fingerprint::is_masked(Dimension::...), so there is one rule, and a provider defines only a setting that is a Value. A policy preset never changes a setting.

Fingerprint::validate() returns a CoherenceReport of errors and warnings (platform and user agent against the OS persona, a WebGL renderer from another OS’s graphics API, a locale from another country than the time zone, and so on). Add a rule there when introducing inter-dependent fields.

Provider execution order#

Providers are sorted by priority and run only when applies(fp) returns true. Current priority bands:

  • 5 — infrastructure (native_to_string).
  • 20–50 — navigator, screen, WebGL, plugins, codecs, permissions.
  • 60–90 — WebDriver/headless cleanup, runtime, iframe, chrome object fixes.
  • 100–130 — self-defense, notification, memory, battery, connection, WebRTC.

Lower numbers run first. A provider that makes replacements look native starts its snippet with providers::SB_PREAMBLE and keeps its priority above 5, because the native_tostring provider (priority 5) installs the helpers that preamble uses. They live in the scope of the assembled script, not on window.

CDP reactor#

StealthReactor starts a background task that attaches to a CDP target and listens for Fetch.requestPaused events. It can:

  • Override request headers (User-Agent, Accept-Language, etc.).
  • Block or mock responses.
  • Intercept JS/CSS resources to inject the stealth bootstrap.

Add new interceptor rules in reactor.rs by matching on request URL patterns and emitting Fetch.continueRequest / FulfillRequest calls.

Binary patching#

ChromedriverPatcher applies byte-level search/replace patches to a driver binary. Each patch is an EnginePatch:

An EnginePatch

Patches run after the driver is downloaded or located. Backups are written with .sb-backup extension and restored on mismatch. Add marker-detection tests whenever a patch targets a new driver version.

Adding new spoofing code#

A new JavaScript evasion

If the change is a new JavaScript evasion, add a provider in src/stealth/providers/builtin.rs and register it in all().

Launch arguments

If it changes launch arguments, update StealthOptions::apply_to in src/stealth/options.rs or add a helper such as engine_spoofing_args() in src/stealth/patcher.rs.

Binary patches

If it patches the binary, add an EnginePatch in src/stealth/patcher.rs.

Re-export

Re-export new public types from src/stealth/mod.rs and src/lib.rs.

Test it

Add a unit test that exercises the new logic without requiring a live browser.

Adding a macro#

Macros are defined in src/macros.rs and re-exported at the crate root via #[macro_export]:

Define it

Add the macro definition in src/macros.rs using $crate:: for any path.

Document it in code

Include a short doc comment with an ignore example.

Test it

Add a unit test in the #[cfg(test)] module if the macro builds a value that can be asserted without a browser.

Document it

Update docs/tutorials/macros.md with the macro name, signature, and example.

Adding an MCP tool#

The sbrs-mcp binary (--server cdp|driver|sb) is a thin wrapper over the mcp module, built with the mcp-server feature. A server is a Host<S>: a list of ToolDefs and the browser session S they share.

Pick the server file

Pick the server file: src/mcp/cdp.rs (the Pure CDP engine), src/mcp/driver.rs or src/mcp/sb.rs (WebDriver, sharing the handlers in src/mcp/webdriver.rs).

Write the handler

Write an async fn(Ctx<S>, Args) -> Result<Output, ToolError> handler. Read arguments with the typed accessors on Args (str, opt_bool, choice, seconds_or, …), which name the argument in every error. Get the session with ctx.session().await?. Return Output::Text or Output::Json.

Register it

Register a ToolDef::new(name, title, description, Effect, Schema, handler). Choose the Effect that is true of the tool (it becomes the MCP annotation hints) and describe each argument with Prop.

Write files safely

Write files only through ctx.settings().output_path(folder, name), which keeps them inside the output directory.

Test it

Test it: tests/mcp_cdp.rs shows how to drive a tool against the mock browser; tests/mcp_webdriver.rs covers the catalogues.

Check the parity

If the tool exists upstream, tests/parity.rs checks it against parity/api.toml; see Syncing with upstream.

Testing guidelines#

  • Add unit tests for pure helper logic (selectors, translations, HTML parsing, retry and rate-limit policies, JS builders).
  • Avoid tests that require a live browser in the default suite; mark them with #[ignore] or place them under tests/.
  • Use BaseCase::without_session(config) to construct a BaseCase for testing helpers that do not touch the browser.
  • Use run_browser_test for async browser tests so cleanup is awaited after either success or failure. Drop cannot perform async WebDriver cleanup.
  • The default nextest profile sets retries to zero. Add retries only around a measured, transient, and idempotent operation rather than masking a flaky test.
  • Run the full verification matrix before pushing:
Terminal

The checks CI runs today, the low-memory build settings and the tests that need a real Chrome are in Build from source.

Tracing internals#

The crate uses tracing with structured fields. Spans are created around major operations (launch, open, click, patch). Events include:

  • seleniumbase.driver.launch — browser/driver launch.
  • seleniumbase.stealth.patch — binary patch applied.
  • seleniumbase.error — structured error event with error.category, error.transient, and error.hint.

To add tracing to a new component:

Instrumenting a component

For JSON output, for ingestion by log analytics, set SB_LOG_FORMAT=json or call init_tracing_json.

Profile payload mapping#

External profile payloads are normalized in src/profile_payloads/profile.rs. The flow is:

When adding a new JSON field:

Declare it

Add it to the corresponding ProfileParams struct with #[serde(default)].

Convert it

Convert it in fingerprint_builder() (one Spoof per setting, through read and settle), so the flag decides its state.

Leave launch-time settings to the fingerprint

Launch-time settings (to_browser_config(), apply_runtime_overrides()) read the built fingerprint, never the raw payload, so a natural flag launches nothing of that setting.

Test it

Add a parser test and a round-trip coherence test.

Keep parser logic defensive: unknown fields should not fail; invalid values should produce SeleniumBaseError::InvalidConfig with the field name.

Test patterns#

Pure-logic tests#

Pure-logic test

Async tests without browser#

Async test without a browser

Browser lifecycle tests#

Use run_browser_test from src/api/runner.rs:

Browser lifecycle test
Warning

run_browser_test guarantees cleanup runs after either success or failure. Drop cannot await async cleanup, so never construct and drop a browser in a synchronous test.

Marking slow/flaky tests#

Place browser-backed tests in examples/ or under tests/ and gate them with a feature flag. Do not add retries to unit tests unless the operation is measured to be transient and idempotent.

Real-browser integration tests#

tests/browser_smoke.rs exercises the end-to-end path against a real Chrome instance: navigation, assertions, typing, screenshots, and deferred asserts. The tests are #[ignore]d by default and serialized with a shared lock so parallel test threads do not race to bind chromedriver’s port.

The browser-smoke job in .github/workflows/ci.yml runs this suite on every push using the Chrome/chromedriver preinstalled on GitHub-hosted runners.

Performance considerations#

  • Avoid allocating large strings in hot paths. EvasionProvider::script runs once per session, so moderate allocation is fine, but do not read files inside the provider loop.
  • Use tokio::spawn for independent background work (e.g. the CDP reactor).
  • Cache CDP sessions and WebDriver clients rather than reconnecting.
  • For chart/tour generation, build the DOM with std::fmt::Write instead of repeated string concatenation.
  • Run cargo build --release when benchmarking; debug builds are 10–100× slower.

How to run the harnesses in benches/, and compare a change fairly, is in Benchmarking.

Security considerations#

  • Never hardcode secrets. BrowserConfig proxy passwords and cloud credentials must come from environment variables or a secrets manager.
  • Validate all user-provided URLs before navigation to prevent SSRF.
  • Do not deserialize untrusted data with pickle-equivalent formats. Profile payloads use serde_json with explicit structs.
  • The MCP server runs browser automation on behalf of clients. It should only be exposed to trusted local clients; see docs/help/mcp_server.md for the trust boundary.
  • Binary patches operate on local files. Always create backups and verify checksums before writing.

Debugging tips#

  • Set RUST_LOG=seleniumbase_rs=debug to see driver launch args and CDP traffic.
  • Use RUST_BACKTRACE=1 for the backtrace of a panic.
  • Run sbrs doctor to print environment diagnostics: the SB_* settings, the Chrome it found, and the chromedriver it found with its version next to Chrome’s.
  • Inspect generated bootstrap with sbrs stealth-bootstrap --fingerprint ....
  • For CI-only failures, reproduce with --features s3,azure,gcp,playwright,mcp-server because feature gates change code paths.

A full walkthrough, from the logs to a mocked browser and the driver checks, is in Debugging.

Python importer architecture#

utilities::python_importer is a conservative static converter for common SeleniumBase and Selenium WebDriver Python statements. It uses a balanced statement and argument scanner plus targeted patterns. It deliberately avoids executing Python or guessing dynamic values.

The importer separates parsing from rendering through typed actions and locators. Unsupported statements produce source-located diagnostics and compiling TODO comments. Keep that behavior when adding mappings: uncertain conversion must remain visible rather than silently changing test semantics.

Add unit tests for both SeleniumBase and Selenium forms when introducing a new mapping. Generated tests should use run_browser_test and remain valid Rust even when diagnostics are present.

MCP server#

The sbrs-mcp binary exposes a subset of BaseCase through the Model Context Protocol. It is built by the package seleniumbase-rs-cli (crates/cli/src/mcp_server.rs) when its mcp-server feature is enabled, which it is by default. See src/mcp/ for the tool definitions and README.md for client configuration examples.

Publishing#

The crate is published from the crate-only tree (seleniumbase-rs/main). All dependencies must come from crates.io (no git-only dependencies). Run cargo publish --dry-run before tagging a release.

Contributing#

See CONTRIBUTING.md for the full contributor guide. In short:

  1. Keep the public API backward-compatible when possible.
  2. Follow the existing module organization.
  3. Write doc comments for public items.
  4. Update README.md, DOCS.md, and docs/tutorials/ when adding user-facing features.
  5. Run cargo fmt before committing.

See also#

AI agent conventions#

This repository is designed to be agent-friendly:

  • Public APIs are typed explicitly; use types as hints.
  • Each module has a focused responsibility; do not add unrelated logic.
  • When extending stealth, providers are the preferred plugin point.
  • When adding docs, mirror the existing structure in docs/SUMMARY.md.
  • Run the verification matrix after any non-trivial change.
  • COPILOT.md contains additional guidance for autonomous work.