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
| Mode | Behaviour | When to use |
|---|---|---|
Exact | Byte-exact comparison. | When trailing whitespace is semantically meaningful. |
TrimTrailing (default) | Trim trailing whitespace per line. | Most widget / screen tests. |
Fuzzy | Collapse 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 wheneverfg,bg, or style flags change; resets at row boundaries. Used byassert_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}.snapor{name}__{profile}.snap. - ANSI:
{name}.ansi.snapor{name}__{profile}.ansi.snap. - The
__{profile}suffix is added automatically whenFTUI_TEST_PROFILEis set.
Updating snapshots
Run the failing test once to see the diff
cargo test -p ftui-demo-showcase dashboardFrankenTUI prints the expected vs actual text and the line-level diff.
Bless the new output
BLESS=1 cargo test -p ftui-demo-showcase dashboardBLESS=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-showcaseThis 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 dashboardCross-profile comparison can be flipped between modes:
FTUI_TEST_PROFILE_COMPARE | Behaviour |
|---|---|
none (default) | Each profile compared only to its own baseline. |
report | Diffs across profiles reported, test still passes. |
strict | Diffs across profiles fail the test. |
Writing a new snapshot test
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.