Skip to Content
TestingSnapshot tests

Snapshot tests

Every demo screen and every widget has a snapshot. A snapshot is the expected on-screen output, captured as text (optionally with ANSI escapes), checked into version control, and compared against fresh renders on every test run. If the output changes, the test fails and prints a unified diff.

Source: crates/ftui-harness/src/lib.rs (snapshot macros, buffer-to-text conversion, match modes) and crates/ftui-demo-showcase/tests/ (46 screens worth of .snap files).

The macros

ftui-harness exports three assertion macros.

use ftui_harness::{assert_snapshot, assert_snapshot_ansi, MatchMode}; // Default: MatchMode::TrimTrailing — strip trailing whitespace per line. assert_snapshot!("my_widget_basic", &buf); // Explicit mode. assert_snapshot!("my_widget_exact", &buf, MatchMode::Exact); // ANSI snapshot — preserves foreground/background/attribute state. assert_snapshot_ansi!("my_widget_styled", &buf);

Match modes

ModeBehaviourWhen to use
ExactByte-exact comparison.When trailing whitespace is semantically meaningful.
TrimTrailing (default)Trim trailing whitespace per line.Most widget / screen tests.
FuzzyCollapse whitespace runs, trim lines.Text that reflows but should “say the same thing”.

Plain vs ANSI

  • buffer_to_text(&buf) — plain grid. Wide-character continuation cells and grapheme pool references render as ? fill to match display width.
  • buffer_to_text_with_pool(&buf, Some(&pool)) — resolves grapheme pool references to the actual multi-codepoint clusters (e.g. emoji with variation selectors). Required for exact text reproduction of complex scripts.
  • buffer_to_ansi(&buf) — emits SGR sequences whenever fg, bg, or style flags change; resets at row boundaries. Used by assert_snapshot_ansi!.

Snapshot file layout

Snapshots live under tests/snapshots/ relative to CARGO_MANIFEST_DIR:

crates/ftui-demo-showcase/tests/snapshots/ ├── dashboard.snap ├── dashboard__dumb.snap ← FTUI_TEST_PROFILE=dumb baseline ├── dashboard__tmux.snap ← FTUI_TEST_PROFILE=tmux baseline ├── widget_gallery.snap ├── widget_gallery.ansi.snap ← ANSI variant └── …
  • Plain text: {name}.snap or {name}__{profile}.snap.
  • ANSI: {name}.ansi.snap or {name}__{profile}.ansi.snap.
  • The __{profile} suffix is added automatically when FTUI_TEST_PROFILE is set.

Updating snapshots

Run the failing test once to see the diff

cargo test -p ftui-demo-showcase dashboard

FrankenTUI prints the expected vs actual text and the line-level diff.

Bless the new output

BLESS=1 cargo test -p ftui-demo-showcase dashboard

BLESS=1 tells ftui-harness to overwrite the .snap file with the current render.

Review every changed .snap

Use cargo insta review if the crate integrates insta, or a plain diff otherwise:

git diff -- crates/ftui-demo-showcase/tests/snapshots/

Read the diff line by line. Intentional? Commit. Unintentional? Revert and fix the code.

Commit the updated snapshots alongside the code change

Snapshots are source. A PR that changes rendering must include the updated .snap files. See contributing: snapshot blessing.

The 46-screen sweep

ftui-demo-showcase enumerates 46 screens — dashboards, widget galleries, text-effects labs, modal stacks, VFX harnesses. Each one has a corresponding snapshot test. The demo binary also exposes per-screen rendering via an env var:

FTUI_HARNESS_VIEW=dashboard cargo run -p ftui-demo-showcase FTUI_HARNESS_VIEW=widget_gallery cargo run -p ftui-demo-showcase

This is what the scripts/demo_showcase_screen_sweep_e2e.sh harness drives in CI. See E2E scripts.

Profile-matrix snapshots

A single widget may render differently on dumb vs tmux vs xterm-256color. To pin a snapshot to a profile:

FTUI_TEST_PROFILE=dumb cargo test -p ftui-harness FTUI_TEST_PROFILE=tmux cargo test -p ftui-harness widget_snapshots FTUI_TEST_PROFILE=modern BLESS=1 cargo test -p ftui-demo-showcase dashboard

Cross-profile comparison can be flipped between modes:

FTUI_TEST_PROFILE_COMPAREBehaviour
none (default)Each profile compared only to its own baseline.
reportDiffs across profiles reported, test still passes.
strictDiffs across profiles fail the test.

Writing a new snapshot test

crates/ftui-widgets/tests/panel_snapshots.rs
use ftui_harness::assert_snapshot; use ftui_render::{buffer::Buffer, frame::Frame, grapheme_pool::GraphemePool}; use ftui_widgets::Panel; #[test] fn panel_with_title_renders() { let mut pool = GraphemePool::new(); let mut frame = Frame::new(20, 5, &mut pool); Panel::new().title("Hello").render(&mut frame, (0, 0, 20, 5)); assert_snapshot!("panel_with_title", &frame.buffer); }

First run: BLESS=1 cargo test -p ftui-widgets panel_with_title_renders. Commit the generated tests/snapshots/panel_with_title.snap. Every future run compares against it.

Pitfalls

Don’t BLESS=1 reflexively. A diff is failure evidence. Read it every time. If you don’t understand why the output changed, you are about to check in a regression.

Grapheme pool references. If buffer_to_text emits ? where you expected an emoji, pass the pool: buffer_to_text_with_pool(&buf, Some(&pool)). The default helper can’t resolve complex clusters without it.

Profile leakage. Running cargo test without FTUI_TEST_PROFILE uses the detected terminal. In CI that can be dumb, locally it can be xterm-256color. If your snapshots diverge, pin the profile explicitly in CI and in the test’s preamble.