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.
What you will learn
- How the repository is organized.
- How
BaseCasemethods are split across domain files. - How to add a new
BaseCasehelper or evasion provider. - How to run tests and interpret CI checks.
- Where to find the public API contract and stability notes.
Clone, build, keep builds small, and run the checks and the real-Chrome tests.
Logs, headed and slowed-down runs, evidence on failure, and a mocked browser.
The cpu and cdp_latency harnesses, and how to compare a change.
Code style, testing rules and how to open a pull request.
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
docsTutorials and help pages
src
apiPublic test API; more modules than the ones listed
browserBrowser launch, session, and configuration
stealthStealth / undetected automation helpers
crates
tools
BaseCase architecture#
BaseCase is a single struct that owns the test state:
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
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:
Capability traits#
The public API is also exposed through capability traits in
src/api/traits.rs:
BrowserApi- navigation and lifecycleElementApi- element finding and interactionAssertionApi- test assertionsScreenshotApi- 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:
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#
| Category | Example variants | Typical cause |
|---|---|---|
| Element | ElementNotFound, ElementNotVisible, ElementNotInteractable | Selector did not match or element is hidden. |
| Driver | DriverNotReady, DriverCrashed, SessionNotCreated | chromedriver is missing, incompatible, or crashed. |
| Config | InvalidConfig, UnsupportedBrowser, InvalidUrl | Bad BrowserConfig or profile payload. |
| I/O | IoError, DownloadFailed, FileNotFound | Filesystem or network transfer failure. |
| Assert | AssertionFailed, DeferredAssertFailed, Skipped | Test assertion mismatch or explicit skip. |
| CDP | CdpError, CdpMethodFailed | Chrome DevTools Protocol call failed. |
| GUI | GuiInputError, OcrError | Desktop automation or image parsing failure. |
| Playwright | PlaywrightError | Playwright runtime error. |
Helper constructors#
Prefer constructors over manual formatting:
Runtime diagnostics#
Errors can emit tracing::error! events:
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:
| Feature | Default | Purpose |
|---|---|---|
tui | off | Terminal apps: sbrs commander, sbrs caseplans, sbrs behave-gui. Pulls ratatui/crossterm. |
gui | off | Native OS dialogs and GUI automation. Pulls rfd/enigo; on Linux it needs GTK and X11 development packages. |
playwright | off | Enables the rustwright-based Playwright-compatible engine. |
s3 | off | Enables AWS S3 artifact uploads. |
azure | off | Enables Azure Blob Storage artifact uploads. |
gcp | off | Placeholder for Google Cloud integrations. |
mcp-server | off | Builds the sbrs-mcp binary using rmcp. |
full-tracing | off | Verbose tracing span events for debugging. |
error-backtrace | off | Declared 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:
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— serializableBrowserConfig,Browserenum,DriverMode.session.rs—BrowserSession, the WebDriver wrapper and CDP session cache.launcher.rs— chromedriver discovery, startup, andStealthOptionsapplication.playwright.rs— optional Playwright-backed session adapter.
src/stealth/#
Anti-detection layer:
fingerprint.rs—Fingerprint,FingerprintBuilder,Dimension, presets (includingedge_desktop), coherence validation, andBrowserType;spoof.rs(Spoof<T>),settings.rs(the value types such asScreenSizeandWebGl) andpolicy.rs(StealthPolicy).evasions.rs/providers/— JavaScript evasion registry and 25 built-in providers.patcher.rs—ChromedriverPatcherandEnginePatchdefinitions.options.rs—StealthOptionsbuilder for launch args and prefs.reactor.rs— background CDPFetchinterceptor.dprocess.rs— detached chromedriver/browser process helpers.humanize.rs— Bézier mouse paths and keystroke timing, and the seedableRngthe rest of the layer shares.randomize.rs— the tables and assembly behindFingerprint::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— thesbrsCLI command tree, declared with theusagederives (andsrc/report.rs,sbrs report). The usage speccompletions/sbrs.usage.kdland the pagedocs/help/cli_reference.mdare generated from it by a test.src/fig.rs— the Fig spec ofsbrs completions fig, written from the same tables.crates/cli/src/mcp_server.rs—sbrs-mcp, which starts the Model Context Protocol server ofsrc/mcp/(the tools areToolDefs there).tests/— every test that runs the realsbrsorsbrs-mcpbinary.
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:
- Launch configuration —
StealthOptionsemits Chromium args and prefs. - Runtime JavaScript —
EvasionProviders generate a bootstrap script. - CDP-level overrides —
evasions::safe_cdp_overrides()sendsNetwork.setUserAgentOverride,Emulation.setDeviceMetricsOverride,Emulation.setTimezoneOverride,Emulation.setLocaleOverride,Emulation.setGeolocationOverride, and related commands. WhenStealthPolicy::native_spoofingis enabled, providers skip JS patches for dimensions the browser can spoof natively, making the spoof invisible to page-sidetoString()/ descriptor inspection. - Network interception —
StealthReactoruses CDPFetchto override headers and responses. - Binary patching —
ChromedriverPatcheredits 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:
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 undertests/. - Use
BaseCase::without_session(config)to construct aBaseCasefor testing helpers that do not touch the browser. - Use
run_browser_testfor async browser tests so cleanup is awaited after either success or failure.Dropcannot 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:
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 witherror.category,error.transient, anderror.hint.
To add tracing to a new 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#
Async tests without browser#
Browser lifecycle tests#
Use run_browser_test from src/api/runner.rs:
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::scriptruns once per session, so moderate allocation is fine, but do not read files inside the provider loop. - Use
tokio::spawnfor 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::Writeinstead of repeated string concatenation. - Run
cargo build --releasewhen 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.
BrowserConfigproxy 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 useserde_jsonwith 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.mdfor the trust boundary. - Binary patches operate on local files. Always create backups and verify checksums before writing.
Debugging tips#
- Set
RUST_LOG=seleniumbase_rs=debugto see driver launch args and CDP traffic. - Use
RUST_BACKTRACE=1for the backtrace of a panic. - Run
sbrs doctorto print environment diagnostics: theSB_*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-serverbecause 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:
- Keep the public API backward-compatible when possible.
- Follow the existing module organization.
- Write doc comments for public items.
- Update
README.md,DOCS.md, anddocs/tutorials/when adding user-facing features. - Run
cargo fmtbefore 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.mdcontains additional guidance for autonomous work.