Testland
Browse all skills & agents

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`.

Install with skills.sh (any agent)

npx skills add testland/qa --skill tui-snapshot-tester
View source

tui-snapshot-tester

Overview

Per textual-testing (opens in new window):

"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 (opens in new window):

pip install pytest-textual-snapshot

This pulls Textual + the snap_compare pytest fixture.

Step 2 - First snapshot test

Per txt (opens in new window) (verbatim):

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

First run fails (no baseline). Per txt (opens in new window):

"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 (opens in new window):

# 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 (opens in new window):

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 (opens in new window):

"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 (opens in new window) - 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.

Related skills

bats-testing

Configures Bats-core (Bash Automated Testing System) for testing CLI tools, shell scripts, and Unix programs - `.bats` test files with `@test` blocks, `run` to capture command exit + output, `[ "$status" -eq 0 ]` and `[ "$output" = ... ]` assertions, `setup`/`teardown` hooks, `load` for shared helpers, parallel execution via `--jobs N`, TAP-compliant output for CI integration. Use whenever the unit-under-test is a shell script, CLI binary, or anything invokable from Bash.

cli-output-conventions

Conventions for designing AND testing CLI output so it stays parseable and assertable - exit-code policy (0 success, non-zero failure with stable codes per failure mode), `stdout` for primary data / `stderr` for messages, `--json` / `--plain` for machine-readable output, deterministic ordering and timestamps, `NO_COLOR` / TTY-aware color, `-q` / `--verbose` discipline, and stable `--help` / `--version`. Built on the [Command Line Interface Guidelines][clig]. Use when designing or testing a CLI's output - a new flag, command, or error message, writing CLI assertions, or fixing flaky tests caused by non-deterministic output; it is the assertion contract for `bats-testing` and tells `tui-snapshot-tester` what does NOT need a snapshot.

pester-cli-testing

Configures Pester v5 for testing PowerShell CLIs, scripts, and cmdlets - Describe/Context/It blocks, Should assertions, Mock for isolating external dependencies, BeforeAll/BeforeEach setup hooks, Invoke-Pester with PesterConfiguration for tags, code coverage, and NUnit/JUnit XML output in CI. Use when the unit-under-test is a PowerShell script, function, or CLI tool invoked from pwsh on Windows or cross-platform.