CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/tui-snapshot-tester

Snapshot testing for terminal UI apps (TUIs) - captures rendered terminal frames as deterministic SVG / text snapshots, diffs them on every run, surfaces a reviewable HTML report on failure, and supports `--snapshot-update` to accept changes intentionally. Wraps `pytest-textual-snapshot` for Python Textual apps; provides equivalent recipes for Ratatui (Rust) `insta` snapshots, Charm Bracelet (Go) `teatest` golden files, and Ink (Node) `ink-testing-library`. Use for any TUI where layout regressions otherwise reach users via `screenshot looks wrong in terminal`.

77

Quality

97%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
tui-snapshot-tester
description:
Snapshot testing for terminal UI apps (TUIs) - captures rendered terminal frames as deterministic SVG / text snapshots, diffs them on every run, surfaces a reviewable HTML report on failure, and supports `--snapshot-update` to accept changes intentionally. Wraps `pytest-textual-snapshot` for Python Textual apps; provides equivalent recipes for Ratatui (Rust) `insta` snapshots, Charm Bracelet (Go) `teatest` golden files, and Ink (Node) `ink-testing-library`. Use for any TUI where layout regressions otherwise reach users via `screenshot looks wrong in terminal`.

tui-snapshot-tester

Overview

Per textual-testing:

"Snapshot testing is the process of recording the output of a test, and comparing it against the output from previous runs." Textual's pytest-textual-snapshot plugin does this by saving an SVG screenshot of the running app to disk.

A TUI snapshot test:

  1. Renders the TUI to a deterministic frame (SVG / text).
  2. Compares the new frame to a stored baseline.
  3. Fails on diff; produces an HTML diff report so the human reviews the change.
  4. Updates the baseline only when the human says so.

This catches layout regressions (clipped text, wrong column widths, broken borders) that exit-code / output assertions miss.

When to use

  • The unit-under-test is a TUI (Textual / Ratatui / Bubble Tea / Ink / curses).
  • The TUI's value is its visual layout - assertions on individual cells are brittle and miss "the box border now looks broken."
  • The team needs PR-time review of layout changes (vs catching them in user feedback).

For headless CLIs that emit text only, prefer bats-testing + cli-output-conventions; TUI snapshots are only needed for layout-rich UIs.

Step 1 - Install (Python / Textual)

Per txt:

pip install pytest-textual-snapshot

This pulls Textual + the snap_compare pytest fixture.

Step 2 - First snapshot test

Per txt (verbatim):

def test_calculator(snap_compare):
    assert snap_compare("path/to/calculator.py")

First run fails (no baseline). Per txt:

"Only ever run pytest with --snapshot-update if you're happy with how the output looks on the left hand side of the snapshot report."

Workflow:

  1. Run pytest. It fails; emits an HTML report at snapshot_report.html.
  2. Open the report. The "left hand side" shows the new render; the "right hand side" shows the saved baseline.
  3. If the new render is correct, run pytest --snapshot-update to accept it.
  4. Commit the updated __snapshots__/ directory.

Step 3 - Drive interactions before capture

Per txt:

# Press keys before snapshotting
assert snap_compare("path/to/calculator.py", press=["1", "2", "3"])

# Custom terminal dimensions
assert snap_compare("path/to/calculator.py", terminal_size=(50, 100))

# Run setup code (mouse hover, etc.)
async def run_before(pilot) -> None:
    await pilot.hover("#number-5")

assert snap_compare("path/to/calculator.py", run_before=run_before)

The Pilot API per txt:

async def test_keys():
    app = RGBApp()
    async with app.run_test() as pilot:
        await pilot.press("r")
        assert app.screen.styles.background == Color.parse("red")

pilot.press() / pilot.click() / pilot.pause() simulate user input.

Step 4 - CI integration (Textual)

Per txt:

"work well in CI on all supported operating systems, and the snapshot report is just an HTML file which can be exported as a build artifact."

jobs:
  tui-snapshot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with: { python-version: '3.13' }
      - run: pip install pytest-textual-snapshot
      - name: Run snapshot tests
        run: pytest tests/ui/
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: snapshot-report, path: snapshot_report.html }

Reviewer downloads snapshot_report.html from the failed CI artifact, opens it locally, and decides accept / reject.

Step 5 - Equivalent for Rust / Ratatui

Use insta for snapshot testing:

# Cargo.toml
[dev-dependencies]
insta = "1"
ratatui = { version = "0.29", features = ["test-buffer"] }
#[test]
fn renders_main_view() {
    let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
    terminal.draw(|f| draw_main(f, &app_state())).unwrap();
    insta::assert_snapshot!(terminal.backend().to_string());
}

Update with cargo insta accept (or cargo insta review for interactive triage). Snapshots stored in tests/snapshots/.

Step 6 - Equivalent for Go / Bubble Tea

Use teatest:

func TestApp(t *testing.T) {
    tm := teatest.NewTestModel(t, NewModel(), teatest.WithInitialTermSize(80, 24))
    tm.Send(tea.KeyMsg{Type: tea.KeyEnter})

    teatest.RequireEqualOutput(t, tm.FinalOutput(t,
        teatest.WithFinalTimeout(2*time.Second)))
}

teatest.RequireEqualOutput writes / compares against testdata/<test-name>.golden. Update with -update:

go test -update ./...

Step 7 - Equivalent for Node / Ink

Use ink-testing-library:

import { render } from 'ink-testing-library';
import App from './app';

test('renders welcome screen', () => {
  const { lastFrame } = render(<App />);
  expect(lastFrame()).toMatchSnapshot();
});

Jest's built-in toMatchSnapshot writes to __snapshots__/. Update with jest -u.

Step 8 - Determinism

Snapshots fail randomly if any of these vary:

  • Date / time in output. Mock the clock (Textual: freezegun; Ratatui: pass now as state; Ink: use vi.useFakeTimers()).
  • Random IDs. Seed PRNGs deterministically.
  • OS-specific glyphs / fonts. Pin OS in CI; warn on cross-OS diffs.
  • Locale. LC_ALL=C in CI.

Without determinism, snapshot tests become noise → team disables.

Step 9 - Snapshot review discipline

Treat snapshot diffs like test diffs in code review:

  • Every snapshot change must be reviewed in PR.
  • The HTML report is the artifact reviewers consult.
  • Auto-update in CI (e.g., bot pushes --snapshot-update commit on diff) defeats the purpose.

The same review discipline applies to browser screenshots (covered by the qa-visual-regression sister plugin).

Anti-patterns

Anti-patternWhy it failsFix
--snapshot-update in CI by defaultDefeats regression detection; baselines drift silently.Updates only on developer machine + reviewed in PR (Step 2).
Snapshots with timestamps / random IDsRandom failures; team disables tests.Mock clock + seed PRNG (Step 8).
One giant snapshot per appAny change forces full baseline review; reviewer skips.Per-screen / per-state snapshots.
Skipping Step 8 (determinism)Cross-OS / cross-machine diffs.LC_ALL=C + pin OS in CI.
No HTML report artifactReviewer can't see the diff; rejects blindly or rubber-stamps.Upload snapshot_report.html artifact (Step 4).

Limitations

  • Brittle to font / terminal width changes. Pin terminal size in tests (Step 3 terminal_size=).
  • Binary diff is harder to review. Per-pixel SVG diffs require the HTML report; raw text diff is unreadable.
  • Slow on large UIs. Each snapshot renders the full UI.
  • Platform divergence. Cross-OS snapshots may diverge by font rendering; restrict CI to one OS for golden files.

References

  • txt - pytest-textual-snapshot, snap_compare, --snapshot-update, Pilot.press / Pilot.click / Pilot.pause, HTML report, CI workflow.
  • insta (Rust): https://insta.rs/.
  • teatest (Go Bubble Tea): https://github.com/charmbracelet/x/tree/main/exp/teatest.
  • ink-testing-library (Node Ink): https://github.com/vadimdemedes/ink-testing-library.
  • bats-testing - exit-code + text output testing (text CLIs); pair with TUI snapshots for layout regression.
Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/tui-snapshot-tester badge