Autumn ships first-class system tests: integration tests that drive a real headless Chromium browser against your running application. Unlike HTTP-level integration tests (which see response bodies but not what the browser does after a response lands), system tests verify the full browser stack — htmx swaps, form flows, SSE-driven DOM updates, and navigation — exactly as a real user would experience them.

Code
┌─────────────────────────┐
│   cargo test --features │
│      system-tests       │
└────────────┬────────────┘
     ┌───────▼────────┐     CDP        ┌──────────────┐
     │ SystemTest API │ ◄───────────── │  Chromium    │
     └───────┬────────┘                └──────────────┘
             │ HTTP
     ┌───────▼────────┐
     │ Autumn app     │  (ephemeral port, in-process)
     └────────────────┘

Quick start

1. Install Chromium

Shell
# Ubuntu / Debian / GitHub Actions ubuntu-latest
sudo apt-get install -y chromium-browser

# macOS (Homebrew)
brew install --cask chromium

# Or set a custom binary path
export AUTUMN_CHROMIUM=/path/to/chrome

2. Add the feature flag

In your app's Cargo.toml:

TOML
[dev-dependencies]
autumn-web = { version = "0.4", features = ["system-tests"] }

[features]
system-tests = ["autumn-web/system-tests"]

Note: The [features] entry is required because generated tests are guarded by #[cfg(feature = "system-tests")] and run commands pass --features system-tests. autumn generate system-test adds both sections automatically.

3. Generate your first test

Shell
autumn generate system-test TodoFlow

This creates tests/system/todo_flow.rs:

Rust
#![cfg(feature = "system-tests")]

use autumn_web::prelude::*;
use autumn_web::system_test::SystemTest;

#[tokio::test]
#[ignore = "requires Chromium — set AUTUMN_CHROMIUM or install chromium-browser"]
async fn todo_flow_index_renders() {
    let runner = SystemTest::new()
        .routes(routes![index])
        .build()
        .await
        .expect("system test runner");

    let page = runner.page().await.expect("page");
    page.visit("/").await.expect("visit /");
    page.expect_text("TodoFlow").await.expect("page title visible");
}

4. Run it

Generated tests are marked #[ignore] by default. Pass -- --include-ignored to actually run them:

Shell
# Run all system tests (requires Chromium)
cargo test --features system-tests -- --include-ignored

# Run a specific test file
cargo test --features system-tests --test todo_flow -- --include-ignored

Page API

All Page methods return Result<&Self, SystemTestError> and can be chained.

MethodDescription
page.visit(path)Navigate to a relative path
page.fill(selector, value)Fill a form input
page.click(selector)Click an element (CSS selector)
page.expect_text(text)Assert text appears in the DOM
page.expect_url(pattern)Assert URL contains pattern
page.expect_attribute(sel, attr, value)Assert element attribute value
page.snapshot()Save a PNG screenshot to artifact dir
page.expect_hx_settle()Explicitly wait for htmx to finish
page.expect_sse_event(id, predicate)Wait for SSE content in DOM

htmx auto-waiting

click() automatically waits for htmx to finish settling before returning. This is implemented by polling document.querySelectorAll('.htmx-request').length === 0 with a 2 s default timeout. You can tune it:

Rust
SystemTest::new()
    .routes(routes![index, action])
    .hx_settle_timeout(Duration::from_secs(5))
    .build()
    .await

Use expect_hx_settle() for an explicit fence after custom JavaScript triggers:

Rust
page.evaluate("htmx.trigger('#myForm', 'submit')").await?;
page.expect_hx_settle().await?;
page.expect_text("Saved").await?;

SSE helper

Rust
page.expect_sse_event("notifications", |text| text.contains("Hello")).await?;

stream_id can be a bare id ("notifications"#notifications) or a full CSS selector ("#notifications", ".sse-target").


Failure artifacts

On any assertion failure, autumn writes two files to target/system-tests/<test-name>/:

FileDescription
<label>.pngFull-page screenshot at the moment of failure
<label>.htmlComplete HTML of the page at the moment of failure
Code
target/system-tests/
└── expect_text/
    ├── expect_text.png
    └── expect_text.html

Override the output directory:

Rust
SystemTest::new()
    .artifact_dir("/tmp/my-artifacts")
    .build()
    .await

Browser resolution

The harness looks for Chromium in this order:

  1. AUTUMN_CHROMIUM environment variable (full path)
  2. PLAYWRIGHT_BROWSERS_PATH directory (scans chromium-*/chrome-linux/chrome)
  3. Common system paths:
    • /usr/bin/chromium-browser (Ubuntu/Debian)
    • /usr/bin/chromium
    • /usr/bin/google-chrome
    • /usr/bin/google-chrome-stable
    • /snap/bin/chromium
    • /Applications/Google Chrome.app/Contents/MacOS/Google Chrome (macOS)
    • /Applications/Chromium.app/Contents/MacOS/Chromium (macOS)

Run autumn doctor to check whether a browser is detected:

Code
🍂 autumn doctor

  ✅ system_test_browser  Chromium for system tests: Chromium 122.0.6261.111 (/usr/bin/chromium-browser)

Test isolation

Each test boots an independent app server on an ephemeral port; the server is shut down when the SystemTestRunner is dropped. Database state is not automatically isolated — use the same teardown patterns from the integration tests guide:

Rust
// Explicit truncate before each browser test
async fn truncate_todos(pool: &Pool<AsyncPgConnection>) {
    diesel::delete(todos::table).execute(&mut *pool.get().await.unwrap()).await.unwrap();
}

#[tokio::test]
#[ignore = "requires Chromium"]
async fn add_todo_flow() {
    let db = TestDb::shared().await;
    truncate_todos(&db.pool()).await;

    let state = AppState::for_test()
        .with_pool(db.pool())
        .with_profile("test");

    let runner = SystemTest::new()
        .routes(routes![index, create_todo])
        .state(state)
        .build()
        .await
        .unwrap();

    let page = runner.page().await.unwrap();
    // ...
}

Transaction-based rollback (from #807) is not yet available for system tests because the browser drives a real HTTP server on a separate TCP connection; per-test transaction wrapping would require a proxy layer not yet shipped.


GitHub Actions setup

Yaml
name: System Tests

on: [push, pull_request]

jobs:
  system-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install Chrome
        # browser-actions/setup-chrome works reliably on all ubuntu-latest
        # versions. Alternatively, Google Chrome is pre-installed on GitHub
        # runners and the harness finds it at /usr/bin/google-chrome
        # automatically — you can omit this step if that binary is present.
        uses: browser-actions/setup-chrome@latest
      - name: Run system tests
        run: cargo test --features system-tests -- --include-ignored
        env:
          RUST_LOG: info

Tip: Rust runs integration tests in parallel by default. If your system tests share state (e.g. a single test database or process-global store), pass --test-threads=1 to serialise them. If they are fully isolated you can raise concurrency with --test-threads=N; see Test isolation above.


Parallelisation caveats

  • Each test spawns its own browser page (tab), not a new browser process. The SystemTestRunner holds one Browser instance; call runner.page() multiple times for concurrent tabs within a single test.
  • Across tests, each SystemTest::build() launches a separate browser process, which can be expensive. For many tests in one binary, consider sharing a browser via a tokio::sync::OnceCell<SystemTestRunner> (page() takes &self so no mutex is needed).
  • --test-threads=1 is the safest default for system tests that share a single test database.

Integrating with autumn generate scaffold

When you run autumn generate scaffold Post title:String body:Text, the scaffold generator also emits a smoke test in tests/<model>.rs. A companion happy-path system test is not generated by default (to avoid requiring Chromium in basic scaffolding workflows), but you can add one immediately:

Shell
autumn generate system-test Post

Checking browser availability

Shell
autumn doctor
# or the dedicated check:
autumn system-test check   # planned; use `autumn doctor` today