Testing
Victauri provides a complete testing toolkit: a typed HTTP client, a Locator API with auto-waiting, assertion helpers, a fluent verification builder, visual regression testing, IPC coverage tracking, and a CLI for running tests from the terminal.
Quick Start
Add the test crate to your dev dependencies:
[dev-dependencies]
victauri-test = "0.9"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Write a test:
#![allow(unused)]
fn main() {
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();
});
}
Run it:
pnpm tauri dev # start your app
VICTAURI_E2E=1 cargo test --test smoke # run tests
Test Client
VictauriClient
The VictauriClient is a typed HTTP client that handles the MCP handshake automatically (the server is stateless by default — there is no session to manage):
#![allow(unused)]
fn main() {
use victauri_test::VictauriClient;
#[tokio::test]
async fn test_my_app() {
let client = VictauriClient::connect(7373).await.unwrap();
let title = client.eval_js("document.title").await.unwrap();
assert!(title.contains("My App"));
client.click("e3").await.unwrap();
client.fill("e5", "hello@example.com").await.unwrap();
}
}
Auto-Discovery
discover() reads the port and auth token from temp files written by the plugin:
#![allow(unused)]
fn main() {
let mut client = VictauriClient::discover().await.unwrap();
}
With Authentication
#![allow(unused)]
fn main() {
let client = VictauriClient::connect_with_token(7373, "my-secret-token")
.await
.unwrap();
}
Direct Client Methods
High-level methods that find elements by text or ID — no ref handles or selectors needed:
| 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 |
double_click_by_id("item") | Find element by id, double-click it |
hover("e3") | Hover over element by ref |
scroll_to_by_id("footer") | Scroll element into viewport |
Low-Level Client Methods
For direct MCP tool access using ref handles:
| Method | Description |
|---|---|
eval_js(expr) | Evaluate JavaScript |
dom_snapshot() | Get full DOM tree |
find_elements(selector) | Find elements by CSS |
click(ref_id) | Click element |
fill(ref_id, value) | Fill input |
type_text(ref_id, text) | Type characters |
press_key(key) | Press keyboard key |
screenshot(label) | Capture PNG |
get_window_state(label) | Window position/size |
list_windows() | All window labels |
invoke_command(name, args) | Call Tauri command |
get_ipc_log(limit) | IPC call history |
get_registry() | Registered commands |
get_memory_stats() | Process memory |
verify_state(frontend, backend) | Cross-boundary check |
detect_ghost_commands() | Unregistered commands |
check_ipc_integrity() | IPC health |
assert_semantic(expr, label, cond, expected) | Semantic assertion |
wait_for(condition, value, timeout) | Wait for condition |
start_recording(session_id) | Begin time-travel |
stop_recording() | End recording |
logs("console", limit) | Console / network / IPC entries |
audit_accessibility() | WCAG audit |
get_performance_metrics() | Navigation timing, heap, resources |
query_db(sql, db_path, params) | SQLite query |
app_info() | App config and paths |
Locator API
For complex queries, Victauri provides composable locators with auto-waiting expectations.
Factory Methods
Create locators by different strategies:
| Factory | Example | Finds by |
|---|---|---|
Locator::role("button") | ARIA role | role="button" |
Locator::text("Submit") | Visible text (substring) | textContent |
Locator::text_exact("OK") | Visible text (exact match) | textContent |
Locator::test_id("login-btn") | Test ID attribute | data-testid |
Locator::css(".nav > a") | CSS selector | CSS query |
Locator::label("Email") | Associated label text | <label> + for |
Locator::placeholder("Search...") | Placeholder attribute | placeholder |
Locator::alt_text("Logo") | Alt text (images) | alt |
Locator::title("Close") | Title attribute | title |
Refinement
Narrow results with chained filters:
#![allow(unused)]
fn main() {
// Button with role "button" AND text containing "Save"
let save = Locator::role("button").and_text("Save");
// Third item in a list
let third = Locator::role("listitem").nth(2);
// Input with specific tag
let textarea = Locator::label("Description").and_tag("textarea");
}
Actions
Interact with resolved elements:
#![allow(unused)]
fn main() {
locator.click(&mut client).await?;
locator.double_click(&mut client).await?;
locator.fill(&mut client, "value").await?;
locator.type_text(&mut client, "typed").await?;
locator.press_key(&mut client, "Enter").await?;
locator.press_key(&mut client, "Control+a").await?; // keyboard combos
locator.hover(&mut client).await?;
locator.focus(&mut client).await?;
locator.scroll_into_view(&mut client).await?;
locator.select_option(&mut client, &["dark"]).await?;
locator.check(&mut client).await?; // checkbox
locator.uncheck(&mut client).await?;
}
Queries
Read element state:
#![allow(unused)]
fn main() {
let text = locator.text_content(&mut client).await?;
let value = locator.input_value(&mut client).await?;
let visible = locator.is_visible(&mut client).await?;
let enabled = locator.is_enabled(&mut client).await?;
let checked = locator.is_checked(&mut client).await?;
let focused = locator.is_focused(&mut client).await?;
let count = locator.count(&mut client).await?;
let bounds = locator.bounding_box(&mut client).await?;
let attr = locator.get_attribute(&mut client, "href").await?;
let all = locator.all(&mut client).await?; // all matches
let texts = locator.all_text_contents(&mut client).await?;
}
Expectations
Auto-waiting assertions with configurable timeout:
#![allow(unused)]
fn main() {
// Wait up to 5s (default) for element to become visible
locator.expect(&mut client).to_be_visible().await?;
// Custom timeout and polling
locator.expect(&mut client)
.timeout_ms(10_000)
.poll_ms(100)
.to_have_text("Complete")
.await?;
// Negation — wait until condition is NOT true
locator.expect(&mut client).not().to_be_visible().await?;
}
| Expectation | Waits until |
|---|---|
.to_be_visible() | Element is visible |
.to_be_hidden() | Element is hidden |
.to_be_enabled() | Element is enabled |
.to_be_disabled() | Element is disabled |
.to_be_focused() | Element has focus |
.to_have_text("exact") | Text content equals value |
.to_contain_text("partial") | Text content contains value |
.to_have_value("input-val") | Input value equals value |
.to_have_attribute("href", "/home") | Attribute equals value |
.to_have_count(3) | Exactly N elements match |
.to_be_checked() | Checkbox/radio is checked |
.to_be_unchecked() | Checkbox/radio is unchecked |
.to_be_attached() | Element exists in DOM |
.to_be_detached() | Element removed from DOM |
Full Locator Example
#![allow(unused)]
fn main() {
use victauri_test::prelude::*;
#[tokio::test]
async fn settings_flow() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
let save_btn = Locator::role("button").and_text("Save");
let email = Locator::label("Email address");
let toast = Locator::test_id("toast-message");
email.fill(&mut client, "user@example.com").await.unwrap();
save_btn.click(&mut client).await.unwrap();
toast.expect(&mut client)
.to_contain_text("Settings saved")
.await
.unwrap();
toast.expect(&mut client)
.timeout_ms(10_000)
.not()
.to_be_visible()
.await
.unwrap();
}
}
Zero-Boilerplate Tests
The e2e_test! macro handles skip-when-no-server and auto-connect:
#![allow(unused)]
fn main() {
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();
});
}
Managed App Lifecycle
TestApp starts your app, waits for the server, and cleans up on drop:
#![allow(unused)]
fn main() {
use victauri_test::TestApp;
#[tokio::test]
async fn full_lifecycle() {
let app = TestApp::spawn("cargo run -p my-app").await.unwrap();
let mut client = app.client().await.unwrap();
client.click_by_text("Start").await.unwrap();
client.expect_text("Running").await.unwrap();
// app process is killed when `app` is dropped
}
}
IPC Verification
Assert IPC Calls Happened
#![allow(unused)]
fn main() {
use victauri_test::{assert_ipc_called, assert_ipc_called_with, assert_ipc_not_called};
client.click_by_id("save-btn").await?;
let log = client.get_ipc_log(None).await?;
assert_ipc_called(&log, "save_settings");
assert_ipc_called_with(&log, "save_settings", &json!({"theme": "dark"}));
assert_ipc_not_called(&log, "delete_account");
}
IPC Checkpoints
Isolate assertions to a specific user action:
#![allow(unused)]
fn main() {
let checkpoint = client.create_ipc_checkpoint().await?;
client.click_by_id("save-btn").await?;
let calls = client.get_ipc_calls_since(checkpoint).await?;
assert_eq!(calls.len(), 1);
assert_eq!(calls[0]["command"], "save_settings");
}
Cross-Boundary Verification
Detect when the frontend and backend disagree:
#![allow(unused)]
fn main() {
let result = client.verify_state(
"document.querySelector('.theme-label').textContent",
json!({"theme": "dark"})
).await?;
assert!(result["divergences"].as_array().unwrap().is_empty());
}
Ghost Command Detection
Find orphaned commands — called in the frontend but missing from the backend:
#![allow(unused)]
fn main() {
let ghosts = client.detect_ghost_commands().await?;
assert!(ghosts["confirmed_ghosts"].as_array().unwrap().is_empty(),
"Found ghost commands: {ghosts}");
}
IPC Health Check
Detect stuck, stale, or errored IPC calls:
#![allow(unused)]
fn main() {
let health = client.check_ipc_integrity().await?;
assert!(health["healthy"].as_bool().unwrap());
}
Backend Access
Victauri provides direct access to the Rust backend — no webview proxy needed.
Query SQLite Databases
#![allow(unused)]
fn main() {
let result = client.query_db(
"SELECT * FROM users WHERE active = ?",
None, // auto-discover database
Some(vec![json!(true)]), // bind parameters
).await?;
println!("{} rows", result["row_count"]);
for row in result["rows"].as_array().unwrap() {
println!(" {} ({})", row["name"], row["email"]);
}
}
Inspect App Configuration
#![allow(unused)]
fn main() {
let info = client.app_info().await?;
println!("App: {}", info["config"]["product_name"]);
println!("Data dir: {}", info["paths"]["data"]);
println!("Databases found: {:?}", info["databases"]);
}
Browse and Read Backend Files
#![allow(unused)]
fn main() {
let files = client.list_app_dir(Some("data"), None).await?;
for entry in files["entries"].as_array().unwrap() {
println!(" {} ({} bytes)", entry["name"], entry["size"]);
}
let content = client.read_app_file("settings.json", Some("config")).await?;
println!("{}", content["content"]);
}
End-to-End: UI Action to Database Verification
#![allow(unused)]
fn main() {
client.click_by_id("save-btn").await?;
let log = client.get_ipc_log(None).await?;
assert_ipc_called(&log, "save_settings");
let result = client.query_db(
"SELECT value FROM settings WHERE key = 'theme'",
None, None,
).await?;
assert_eq!(result["rows"][0]["value"], "dark");
}
Fluent Verification
Check multiple conditions at once — DOM, IPC, accessibility, errors — with a single report:
#![allow(unused)]
fn main() {
let report = client.verify()
.has_text("Settings saved")
.has_no_text("Error")
.ipc_was_called("save_settings")
.ipc_was_called_with("save_settings", json!({"theme": "dark"}))
.ipc_was_not_called("delete_account")
.no_console_errors()
.no_ghost_commands()
.ipc_healthy()
.coverage_above(80.0)
.run()
.await?;
report.assert_all_passed();
for failure in report.failures() {
eprintln!("FAILED: {} — {}", failure.description, failure.detail);
}
}
Visual Regression Testing
Compare screenshots against stored baselines with pixel-level diffing:
#![allow(unused)]
fn main() {
use victauri_test::visual::{VisualOptions, ThresholdPreset, MaskRegion};
let opts = VisualOptions {
snapshot_dir: "tests/snapshots".into(),
..VisualOptions::default()
}
.with_preset(ThresholdPreset::Standard);
// Errors with `TestError::VisualRegression` when the diff exceeds the threshold.
let diff = client.screenshot_visual("dashboard", &opts).await?;
println!("{:.2}% of pixels match", diff.match_percentage);
}
On first run, the screenshot is saved as the baseline. Subsequent runs compare and generate a red-overlay diff image when mismatched.
Threshold Presets
| Preset | Tolerance | Threshold | Use case |
|---|---|---|---|
Strict | 0 | 0.0% | Pixel-perfect, no variation |
Standard | 2 | 0.1% | Most apps, minor anti-aliasing OK |
AntiAlias | 5 | 0.5% | Cross-browser font rendering |
Relaxed | 10 | 2.0% | Cross-platform CI |
Mask Regions
Exclude dynamic content from comparison:
#![allow(unused)]
fn main() {
let opts = VisualOptions {
snapshot_dir: "tests/snapshots".into(),
..VisualOptions::default()
}
.with_preset(ThresholdPreset::Standard)
.with_mask(MaskRegion::new(0, 0, 200, 50)); // timestamp header
}
Save Screenshots to Files
#![allow(unused)]
fn main() {
client.screenshot_to_file("debug.png").await?;
client.screenshot_to_file_for("main", "main-window.png").await?;
}
IPC Coverage
Track which registered Tauri commands your tests actually exercise:
#![allow(unused)]
fn main() {
use victauri_test::coverage::coverage_report;
let report = coverage_report(&mut client).await?;
println!("{}", report.to_summary());
assert!(report.meets_threshold(80.0),
"Coverage {:.1}% below 80% threshold", report.coverage_percentage);
}
Or inline with the fluent builder:
#![allow(unused)]
fn main() {
client.verify()
.has_text("Welcome")
.coverage_above(80.0)
.run().await?.assert_all_passed();
}
From the CLI:
victauri coverage --threshold 80
Prerequisite: Commands must use #[inspectable] to be tracked. See Command Instrumentation.
Accessibility Auditing
Run WCAG-based accessibility checks against your running app:
#![allow(unused)]
fn main() {
let audit = client.audit_accessibility().await?;
let violations = audit["summary"]["violations"].as_u64().unwrap_or(0);
assert_eq!(violations, 0, "Accessibility violations found: {audit}");
}
Checks include: images without alt text, unlabeled form inputs, empty buttons/links, heading hierarchy, color contrast (WCAG AA), ARIA role validity, positive tabindex, missing document language and title.
Use the assertion helper for a one-liner:
#![allow(unused)]
fn main() {
use victauri_test::assert_no_a11y_violations;
let audit = client.audit_accessibility().await?;
assert_no_a11y_violations(&audit);
}
Performance Monitoring
Track navigation timing, memory usage, and resource loading:
#![allow(unused)]
fn main() {
let metrics = client.get_performance_metrics().await?;
let heap_mb = metrics["heap"]["usedJSHeapSize"]
.as_f64().unwrap_or(0.0) / 1_048_576.0;
assert!(heap_mb < 256.0, "Heap usage too high: {heap_mb:.1} MB");
let load_ms = metrics["navigation"]["loadEventEnd"]
.as_f64().unwrap_or(0.0);
assert!(load_ms < 3000.0, "Page load too slow: {load_ms:.0}ms");
}
Or use the assertion helper with a budget:
#![allow(unused)]
fn main() {
use victauri_test::assert_performance_budget;
let metrics = client.get_performance_metrics().await?;
assert_performance_budget(&metrics, 5000.0, 512.0); // max load ms, max heap MB
}
Metrics include: DNS lookup time, TTFB, DOM interactive/complete, load event, resource summary (count, transfer size, by type, 5 slowest), paint timing (FP, FCP), JS heap usage, long task count, DOM stats.
Time-Travel Recording
Record interactions, create checkpoints, and generate test files.
Record from the CLI
victauri record --output tests/login_flow.rs --test-name login_flow
# Interact with your app...
# Press Ctrl+C to stop and generate the test file
Record Programmatically
#![allow(unused)]
fn main() {
client.start_recording(Some("my-session")).await?;
client.fill_by_id("email", "user@example.com").await?;
client.click_by_id("login-btn").await?;
// `stop_recording` returns the full session (events + any checkpoints).
let session = client.stop_recording().await?;
// Mid-session checkpoints and events-between-checkpoints are available through the
// `recording` tool's actions (checkpoint / events / events_between) via call_tool,
// or through the core `EventRecorder` API when embedding directly.
}
Command Instrumentation
Mark your Tauri commands with #[inspectable] for coverage tracking, ghost detection, and natural language resolution. The generated code names victauri_core::... by path, so the crate also needs victauri-core (and victauri-macros for this import) in its own [dependencies]:
#![allow(unused)]
fn main() {
use victauri_macros::inspectable;
#[tauri::command]
#[inspectable(
description = "Save user preferences",
intent = "persist settings",
category = "settings",
example = "save the user's theme preference"
)]
async fn save_settings(settings: Settings) -> Result<(), AppError> {
// your code
}
}
This generates a command schema at compile time — zero runtime cost. Commands become discoverable through get_registry and natural language via resolve_command.
To auto-discover all instrumented commands:
#![allow(unused)]
fn main() {
tauri::Builder::default()
.plugin(
victauri_plugin::VictauriBuilder::new()
.auto_discover()
.build()
.unwrap(),
)
// ...
}
Assertion Helpers
Standalone Functions
#![allow(unused)]
fn main() {
use victauri_test::{
assert_json_eq,
assert_json_truthy,
assert_no_a11y_violations,
assert_performance_budget,
assert_ipc_healthy,
assert_state_matches,
};
// These helpers are SYNCHRONOUS and take a `&Value` you already fetched (plus a
// JSON pointer for the *_json_ helpers), not the client. Fetch first, then assert:
let title = client.eval_js("document.title").await?;
assert_json_eq(&title, "", &json!("My App")); // pointer "" = the whole value
let integrity = client.check_ipc_integrity().await?;
assert_ipc_healthy(&integrity);
}
Client Assertion Methods
Each returns Result<(), TestError> — propagate with ? (or .unwrap() in a test):
#![allow(unused)]
fn main() {
use std::time::Duration;
client.assert_eval_works().await?;
client.assert_dom_snapshot_valid().await?;
client.assert_screenshot_ok().await?;
client.assert_windows_exist().await?;
client.assert_ipc_integrity_ok().await?;
client.assert_accessible().await?;
client.assert_dom_complete_under(Duration::from_millis(5000)).await?;
client.assert_heap_under_mb(200.0).await?;
client.assert_no_uncaught_errors().await?;
client.assert_recording_lifecycle().await?;
client.assert_health_hardened().await?;
}
Smoke Test Suite
Run the built-in 11-check smoke test programmatically:
#![allow(unused)]
fn main() {
use victauri_test::{VictauriClient, SmokeConfig};
#[tokio::test]
async fn smoke() {
let client = VictauriClient::connect(7373).await.unwrap();
let report = client.smoke_test().await.unwrap();
println!("Passed: {}/{}", report.passed_count(), report.total_count());
assert!(report.all_passed());
// Custom thresholds
let config = SmokeConfig {
max_dom_complete_ms: 3000,
max_heap_mb: 150.0,
};
let report = client.smoke_test_with_config(config).await.unwrap();
assert!(report.all_passed());
}
}
The 11 checks: eval_js works, DOM snapshot valid, screenshot captures an image, windows exist, IPC integrity healthy, no uncaught errors, accessibility audit, DOM complete under the load budget (--max-load-ms), JS heap under the memory budget (--max-heap-mb), recording lifecycle, and health endpoint hardened.
Reports include timing data and can export to JUnit XML for CI integration.
Common Patterns
Test a Form Submission End-to-End
#![allow(unused)]
fn main() {
#[tokio::test]
async fn submit_contact_form() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
let email = Locator::label("Email");
let message = Locator::label("Message");
let submit = Locator::role("button").and_text("Send");
email.fill(&mut client, "user@example.com").await.unwrap();
message.fill(&mut client, "Hello!").await.unwrap();
submit.click(&mut client).await.unwrap();
Locator::text("Message sent")
.expect(&mut client)
.to_be_visible()
.await
.unwrap();
let log = client.get_ipc_log(Some(1)).await.unwrap();
assert_ipc_called_with(&log, "send_message", &json!({
"email": "user@example.com",
"body": "Hello!"
}));
}
}
Test Navigation Between Pages
#![allow(unused)]
fn main() {
#[tokio::test]
async fn navigation_flow() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
Locator::text("Settings").click(&mut client).await.unwrap();
Locator::role("heading").and_text("Settings")
.expect(&mut client)
.to_be_visible()
.await
.unwrap();
let url = client.eval_js("window.location.hash").await.unwrap();
assert_eq!(url.as_str().unwrap(), "#/settings");
}
}
Test Multi-Window Behavior
#![allow(unused)]
fn main() {
#[tokio::test]
async fn notification_window() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
let windows = client.list_windows().await.unwrap();
let labels: Vec<&str> = windows.as_array().unwrap()
.iter().filter_map(|w| w.as_str()).collect();
assert!(labels.contains(&"main"));
let state = client.get_window_state(Some("main")).await.unwrap();
assert!(state["visible"].as_bool().unwrap());
}
}
Verify State Consistency After Interaction
#![allow(unused)]
fn main() {
#[tokio::test]
async fn counter_state_sync() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
for _ in 0..3 {
client.click_by_id("increment-btn").await.unwrap();
}
client.expect_text("3").await.unwrap();
let result = client.invoke_command("get_counter", None).await.unwrap();
assert_eq!(result.as_i64().unwrap(), 3);
}
}
Full Verification Report in CI
#![allow(unused)]
fn main() {
#[tokio::test]
async fn ci_health_check() {
if !is_e2e() { return; }
let mut client = VictauriClient::discover().await.unwrap();
client.verify()
.has_text("Welcome")
.no_console_errors()
.ipc_healthy()
.no_ghost_commands()
.coverage_above(75.0)
.run().await.unwrap()
.assert_all_passed();
}
}
CLI Reference
Install with cargo install victauri-cli, then:
victauri init # Scaffold test directory with starter tests
victauri check # Connect to running app, report health
victauri check --junit report.xml # Same, with JUnit XML output
victauri test # Run 11 built-in smoke checks
victauri test --max-load-ms 5000 --max-heap-mb 256 # With custom thresholds
victauri record --output tests/flow.rs # Record interactions → generate Rust test
victauri coverage --threshold 80 # Report IPC coverage, fail if below 80%
victauri watch # Re-run tests on file changes
victauri watch --filter smoke # Only re-run specific test file
victauri test — Smoke Suite
Runs 11 built-in checks against your running app:
- Server connectivity
- JavaScript evaluation
- DOM snapshot validity
- Screenshot capture
- Window enumeration
- IPC integrity
- Accessibility audit (violations)
- Accessibility audit (warnings)
- DOM load performance
- Heap memory usage
- Health endpoint hardening
Exit code 0 if all pass, 1 if any fail. Ideal for CI gates.
CI Integration
Victauri tests run in CI without special infrastructure. Pick the approach that fits:
Option A: GitHub Action (recommended)
# .github/workflows/test.yml
- name: Start app
run: xvfb-run --auto-servernum cargo run -p my-app &
- uses: 4DA-Systems/victauri/.github/actions/victauri-test@v0.9.0
with:
max-load-ms: 5000
max-heap-mb: 256
coverage: true
coverage-threshold: 80
junit-path: results.xml
One step. Installs the CLI, waits for the server, runs smoke tests, and optionally gates on IPC coverage.
Option B: Managed Lifecycle with TestApp
# .github/workflows/test.yml
- name: Run Victauri tests
run: cargo test -p my-app --test integration
TestApp::spawn handles starting the app, waiting for the server, and cleanup. Nothing else needed.
Option C: Manual Server Lifecycle
- name: Build app
run: cargo build -p my-app
- name: Start app
run: xvfb-run --auto-servernum cargo run -p my-app &
- name: Wait for server
run: |
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:7373/health && break
sleep 1
done
- name: Run tests
run: cargo test -p my-app --test integration -- --test-threads=1
env:
VICTAURI_E2E: "1"
VICTAURI_PORT: "7373"
Option D: Use the CLI Directly
- name: Start app
run: xvfb-run --auto-servernum cargo run -p my-app &
- name: Smoke test
run: victauri test --junit results.xml --max-load-ms 5000
- name: Coverage gate
run: victauri coverage --threshold 80 --junit coverage.xml
Platform Notes
| Platform | Notes |
|---|---|
| Linux | Requires xvfb-run --auto-servernum for headless display |
| macOS | Works out of the box — no WebDriver/CDP needed |
| Windows | Works out of the box — uses native PrintWindow for screenshots |