Verified Introspection & Control for Tauri Applications
Full-stack testing for Tauri apps. Click a button in the frontend, verify the Rust handler ran, confirm the database row was written — from one test.
Testing Tauri apps today means choosing between frontend mocks that lie about your backend, WebDriver setups that take a weekend, or paying for macOS CI runners. Victauri replaces all three: it embeds a lightweight server inside your Tauri process (debug builds only) that gives your test suite, curl, and CI direct access to the DOM, IPC layer, Rust backend state, the database, and native windows — from one test, on all three platforms. No WebDriver. No browser dependency. Works on macOS, Windows, and Linux.
That same server also speaks MCP, so any AI agent — Claude Code, Cursor, Windsurf — can drive and debug your app with the exact same full-stack access. Testing is the job; the agent integration is the bonus.
Tested against real-world Tauri apps. In a one-time deep evaluation (May 2026) across 5 open-source apps (Kanri, En Croissant, Surrealist, Duckling, Lettura), 867 / 895 checks passed (96.9%) with zero Victauri bugs and zero changes to the apps. A reproducible compat harness (
scripts/compat) re-runs an app-agnostic smoke battery against the current code on demand (and weekly), pinned to three Tauri-2 apps (Kanri, En Croissant, Lettura). It is a best-effort net for upstream drift, not a release gate — third-party apps move on their own schedules and periodically need re-pinning. See the compat README for what it covers and current per-app status.
Note — browser mode removed (2026-06-09). The experimental
victauri-browserextension +@4da/victauri-browsernpm package (MCP for any website) have been removed so Victauri can focus on its strength: full-stack introspection inside Tauri apps. For browser automation, use Playwright or the Chrome DevTools Protocol. The Tauri plugin is unaffected. Details & migration: #15.
For your test suite and CI:
- Full-stack verification — click a button, verify the IPC call, query the database to confirm the write, check the UI updated — in one test
- Direct backend access — query SQLite databases, browse app files, read config, inspect process memory — no webview proxy
- Ghost command detection — find high-confidence orphaned frontend calls (
confirmed_ghosts: invoked but only ever errored "not found"), and backend commands the frontend never calls - Cross-boundary state checking — compare DOM state against Rust backend state and catch the drift between them
- Time-travel recording — record interactions, checkpoint state, replay sequences, generate test files
- Cross-platform, no WebDriver — identical behavior on macOS, Windows, and Linux; runs headless in CI under
xvfb - Zero runtime cost in release — the server is gated behind
#[cfg(debug_assertions)], soinit()is a no-op and nothing listens in release builds. (The crate still compiles in; add it as adev-dependencyif you want it absent from the release binary entirely.)
Bonus — for AI agents: the same server speaks MCP, so Claude Code, Cursor, Windsurf, and any MCP client get this full-stack access for interactive debugging — no extra setup.
Victauri is a build-time dev dependency you add to your own app's source and rebuild — not a tool you attach to an already-running or shipped app. It works when all four hold:
- Tauri 2. Tauri 1.x apps can't host it — their
webkit2gtk-sys 0.18and Victauri's2.xboth link the nativeweb_kit2library, an unresolvable cargo conflict. (Tauri 2 is the current major; Tauri 1 is legacy.) - Built from source with the plugin wired in — one line in
Cargo.toml,.plugin(victauri_plugin::init()), and avictauri:defaultcapability. There is no inject-into-a-foreign-binary path. - A debug build. The server is
#[cfg(debug_assertions)]-gated, soinit()is a no-op and nothing listens in release. It's a dev/test-time tool by design. - Each window grants
victauri:default. Tauri's per-window permission ACL silently blinds the bridge without it; thewindow introspectabilitytool detects and explains this.
Framework (React, Vue/Nuxt, Svelte, vanilla) and OS/webview engine (WebView2 / WKWebView / WebKitGTK) do not matter — all supported and cross-checked on Windows, macOS, and Linux.
| ✅ Works on | ❌ Won't work on |
|---|---|
| Your own Tauri 2 app during development | Tauri 1.x apps |
| Any Tauri 2 app you can build from source (debug) | Release / production builds |
| Any frontend framework, any of the 3 OSes | A binary you didn't build (no source) |
| Non-Tauri apps (Electron, native, plain web) |
Full details, edge cases, and a tested-apps list: Compatibility.
cargo install victauri-cliFrom your Tauri project root:
victauri initThis will:
- Add
victauri-pluginandvictauri-testto yourCargo.toml - Create starter smoke tests in your
tests/directory - Print the next steps to wire the plugin
Add one line to your Tauri builder:
// src-tauri/src/main.rs (or lib.rs)
tauri::Builder::default()
.plugin(victauri_plugin::init())
.invoke_handler(tauri::generate_handler![/* your commands */])
.run(tauri::generate_context!())
.expect("error while running tauri application");In release builds, init() returns a no-op plugin — the server never starts, so there's zero runtime cost and no feature flags needed.
Start your app, then run the smoke suite:
pnpm tauri dev # start your app
VICTAURI_E2E=1 cargo test --test smoke # run testsOr use the CLI for instant validation:
victauri test # 11 built-in smoke checks
victauri check # server health + IPC diagnosticsAdd .mcp.json to your project root (created automatically by victauri init):
{
"mcpServers": {
"my-app": {
"command": "victauri",
"args": ["bridge", "--wait"]
}
}
}The victauri bridge stdio proxy discovers the running app's port at connect time and
re-discovers on restart, so the agent always reaches the right app — even across rebuilds, or
when several Victauri apps are running (add "--app", "<your.bundle.identifier>" to pin one).
Prefer it over a fixed "url": "http://127.0.0.1:7373/mcp", which hardcodes a port and can
bind the wrong app.
Works with Claude Code, Cursor, Windsurf, and any MCP client. Your agent gets full-stack access: DOM snapshots, IPC monitoring, command invocation, screenshot capture, accessibility auditing, and more.
With Claude Code, start your app and Claude can immediately:
- Inspect the DOM tree and click/type/fill any element
- Invoke any
#[tauri::command]and verify the response - Read app config, list files, query SQLite databases
- Take screenshots and audit accessibility
- Record interactions and replay them as tests
The e2e_test! macro handles server detection and auto-connect:
use victauri_test::{e2e_test, VictauriClient};
e2e_test!(greet_flow, |client| async move {
client.fill_by_id("name-input", "World").await.unwrap();
client.click_by_id("greet-btn").await.unwrap();
client.expect_text("Hello, World!").await.unwrap();
});For complex queries, composable locators with auto-waiting expectations:
use victauri_test::prelude::*;
e2e_test!(settings_flow, |client| async move {
let save = Locator::role("button").and_text("Save");
let email = Locator::label("Email address");
email.fill(&mut client, "user@example.com").await.unwrap();
save.click(&mut client).await.unwrap();
Locator::test_id("toast-message")
.expect(&mut client)
.to_contain_text("Settings saved")
.await
.unwrap();
});Locators support role, text, test_id, css, label, placeholder, alt_text, and title strategies with chainable refinement (.and_text(), .nth(), .and_tag()). See the Testing Guide for the full Locator API reference.
| Method | What it does |
|---|---|
click_by_text("Submit") |
Find element by visible text, click it |
click_by_id("save-btn") |
Find element by HTML id, click it |
fill_by_id("email", "a@b.com") |
Find input by id, fill value |
type_by_id("search", "query") |
Find input by id, type char-by-char |
select_by_id("theme", "dark") |
Find select by id, choose option |
expect_text("Success!") |
Poll until text appears (5s timeout) |
expect_no_text("Error") |
Poll until text disappears (3s timeout) |
text_by_id("counter") |
Get text content of element by id |
This is what sets Victauri apart — verifying that frontend actions actually trigger the right backend logic.
// Click "Save" in the UI
client.click_by_id("save-btn").await?;
// Verify the IPC command was called
let log = client.get_ipc_log(None).await?;
assert_ipc_called(&log, "save_settings");
// Verify the database was actually written
let result = client.query_db(
"SELECT value FROM settings WHERE key = 'theme'",
None, None,
).await?;
assert_eq!(result["rows"][0]["value"], "dark");Check multiple conditions at once — DOM, IPC, accessibility, errors — with a single report:
client.verify()
.has_text("Settings saved")
.ipc_was_called("save_settings")
.no_console_errors()
.no_ghost_commands()
.ipc_healthy()
.coverage_above(80.0)
.run()
.await?
.assert_all_passed();Find orphaned commands — called in the frontend but missing from the backend:
let ghosts = client.detect_ghost_commands().await?;
assert!(ghosts["confirmed_ghosts"].as_array().unwrap().is_empty(),
"Found ghost commands: {ghosts}");See the Testing Guide for IPC checkpoints, visual regression testing, IPC coverage, accessibility auditing, performance monitoring, time-travel recording, CI integration, and more.
35 tools across the full stack — backend, IPC, webview, and introspection:
| Tool | What it does |
|---|---|
app_info |
App config, directory paths, env vars, discovered databases, process info |
list_app_dir |
Browse files in app data/config/log/local_data directories |
read_app_file |
Read files from app backend directories (UTF-8 or base64) |
query_db |
Read-only SQLite queries with auto-discovery |
invoke_command |
Call any Tauri command directly through IPC |
app_state |
Read app-defined backend-state probes (pipeline/queue/cache internals) — no IPC round-trip |
get_memory_stats |
Real-time OS process memory (working set, page faults) |
| Tool | What it does |
|---|---|
get_registry |
List all #[inspectable] command schemas |
detect_ghost_commands |
Find orphaned frontend IPC calls (high-confidence confirmed_ghosts) |
check_ipc_integrity |
Detect stuck/stale/errored IPC calls |
verify_state |
Compare frontend DOM against backend state |
resolve_command |
Natural language to matching Tauri command |
| Tool | What it does |
|---|---|
eval_js |
Execute JavaScript in the webview |
dom_snapshot |
Full accessibility tree with ref handles |
find_elements |
Search by text, role, test ID, CSS, label, placeholder, alt, title |
screenshot |
Platform-native window capture (no Chromium) |
wait_for |
Poll for conditions: text, selector, IPC settle, JS expression, or Tauri event — await async backend work without sleeps |
assert_semantic |
Evaluate JS + assert against expected value |
| Tool | Actions |
|---|---|
interact |
click, double_click, hover, focus, scroll, select |
input |
fill, type_text, press_key (keyboard combos supported) |
window |
get_state, list, manage, resize, move, set_title |
storage |
get, set, delete, cookies |
navigate |
go_to, back, history, dialogs |
recording |
start, stop, checkpoint, events, export, import |
inspect |
styles, bounds, highlight, audit_accessibility, get_performance |
logs |
console, network, ipc, navigation, dialogs, events, slow_ipc |
css |
inject, remove |
introspect |
command_timings, coverage, contract_record, contract_check, startup_timing, capabilities, db_health, plugin_state, processes, plugin_tasks, event_bus |
fault |
inject (delay/error/drop/corrupt), list, clear, clear_all |
explain |
summary, last_action, diff |
get_plugin_info |
Plugin config: port, tools, privacy, version |
get_diagnostics |
Shadow DOM, service workers, iframes, large DOM detection |
All tools are also available via REST at POST /api/tools/{name} — no MCP client needed. See the Tools Reference.
AI Agent / cargo test / curl
|
v
HTTP on :7373
├── /mcp (MCP protocol — for AI agents)
├── /api/tools (REST API — for scripts and CI)
└── /health (health check — for monitoring)
|
v
Victauri Plugin (inside Tauri process)
| | |
v v v
WebView IPC Backend
- DOM - log - app config
- click - args - file system
- eval - cmds - SQLite DBs
- a11y - ghost - memory
- perf - verify- env vars
Victauri runs inside the Tauri process — same thread pool, same memory space. This isn't an implementation detail; it changes what's possible:
| Embedded (Victauri) | External process | |
|---|---|---|
| Tool response | <1ms (function call) | 5-50ms (IPC + serialization) |
| State accuracy | Zero drift (reads live state) | Stale (snapshot + transfer) |
| Backend access | Full (AppHandle, DB, state) | Limited (webview only) |
| Setup | One line in Cargo.toml | Separate process + config |
| Release build | init() is a no-op (zero runtime cost) |
Must be disabled manually |
Port selection: Victauri tries port 7373 first, then falls back through 7374-7383 if taken. The actual port is written to a temp directory for automatic client discovery.
victauri/
├── crates/
│ ├── victauri-plugin/ # Tauri plugin + MCP server + JS bridge (the main crate)
│ ├── victauri-core/ # Shared types (events, registry, snapshots, verification)
│ ├── victauri-macros/ # #[inspectable] proc macro for command schemas
│ ├── victauri-test/ # Test client + Locator API + assertion helpers
│ ├── victauri-cli/ # CLI: init, check, test, record, watch, coverage
│ └── victauri-watchdog/ # Health-check sidecar for crash recovery
├── docs/ # mdbook documentation site
└── examples/
└── demo-app/ # Multi-window Tauri app with 21 instrumented commands
| Crate | Purpose | Tauri dependency? |
|---|---|---|
victauri-plugin |
Embed in your app — MCP server + bridge | Yes |
victauri-test |
Use in your tests — client + assertions | No |
victauri-cli |
Install globally — scaffold + diagnose | No |
victauri-macros |
Use on commands — #[inspectable] |
No |
victauri-core |
Shared types (usually not used directly) | No |
victauri-watchdog |
Run as sidecar for crash recovery | No |
Victauri is designed for development, not production:
- Debug-only: the server is
#[cfg(debug_assertions)]-gated, soinit()is a no-op and nothing listens in release builds - Localhost-only: binds to 127.0.0.1, DNS rebinding protection
- Auth on by default: auto-generated Bearer token, auto-discovered by clients (
.auth_disabled()to opt out) - Rate limited: 1000 req/sec, token-bucket algorithm
- Privacy profiles:
Observe(read-only),Test(interactions),FullControl(everything) - Output redaction: auto-scrub API keys, tokens, emails from tool responses
See the Security Guide for threat model, privacy configuration, and command filtering.
Use the built-in composite action to run Victauri smoke tests in CI:
- name: Build my app
run: cargo build -p my-app
- name: Start app under xvfb
run: |
xvfb-run -a ./target/debug/my-app &
sleep 3
- uses: 4DA-Systems/victauri/.github/actions/victauri-test@v0.8.1
with:
max-load-ms: 10000
max-heap-mb: 512The action installs victauri-cli, waits for the server to become healthy, and runs all 11 smoke checks. Available inputs:
| Input | Default | Description |
|---|---|---|
port |
7373 |
Victauri server port |
max-load-ms |
10000 |
Maximum DOM complete time (ms) |
max-heap-mb |
512 |
Maximum JS heap usage (MB) |
junit-path |
Path to write JUnit XML report | |
coverage |
false |
Run IPC coverage report after tests |
coverage-threshold |
Minimum coverage % (fails if below) | |
health-timeout |
30 |
Seconds to wait for server health |
Or use victauri init to generate a complete CI workflow file for your project:
victauri init # generates .github/workflows/victauri.yml- Getting Started — Setup, capabilities, first connection
- Testing Guide — Locator API, IPC verification, visual regression, CI integration
- Tools Reference — All 35 tools with parameters and examples
- Architecture — Embedded design, JS bridge, dual protocol
- Configuration — Port, auth, privacy, capacity tuning
- Security — Threat model, privacy profiles, redaction
- FAQ — Common questions and troubleshooting
- VS Code Extension — Live inspection from your editor
- Demo App — Reference app with 21 instrumented commands
- Agent Session — Real AI agent transcript
- Migration Guide — Upgrading between versions
- Contributing — How to contribute
- Changelog — Release history
- No production use — debug builds only, by design
- No remote access — localhost only, no port forwarding
- Same-origin frames only — same-origin iframes are traversed; cross-origin frames are marked and skipped
- No live frontend-IPC control —
faultinjection applies to commands driven through Victauri's owninvoke_command, not the app's real frontend IPC (that path sits below the layer the JS bridge can reach without CDP) - Pre-1.0 — API may change (semver-checked in CI)
cargo build --workspace # Build all crates
cargo test --workspace # Run all tests
cargo bench -p victauri-core # Criterion benchmarks (16)
cargo clippy --workspace --all-targets # Lint (20 enforced lints)
cargo fmt --all -- --check # Format
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps # Docs (zero warnings)Lint policy: 20 clippy lints (pedantic + nursery) enforced at deny level — see [workspace.lints.clippy] in Cargo.toml.
Victauri is open source and built by 4DA Systems, which uses it to test its own Tauri app. We want it to become the default way to test Tauri apps full-stack — and that needs more than one company.
- Using it on a Tauri app? Tell us — open a discussion or issue. We'd love to add you to a "used by" list and learn what broke.
- Want to contribute? See CONTRIBUTING.md. Good first areas: more
victauri-testassertion helpers, framework-specific testing guides, and CI recipes. - Found a bug or a Tauri app it doesn't work on? File an issue with the app and the failing tool call — those reports are the most valuable thing you can send us.
If Victauri saves you a weekend of WebDriver setup, a ⭐ helps other Tauri developers find it.
Apache-2.0 — LICENSE
Built and maintained by 4DA Systems.
