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-testertui-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-snapshotplugin does this by saving an SVG screenshot of the running app to disk.
A TUI snapshot test:
This catches layout regressions (clipped text, wrong column widths, broken borders) that exit-code / output assertions miss.
When to use
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-snapshotThis 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-updateif you're happy with how the output looks on the left hand side of the snapshot report."
Workflow:
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:
Without determinism, snapshot tests become noise → team disables.
Step 9 - Snapshot review discipline
Treat snapshot diffs like test diffs in code review:
The same review discipline applies to browser screenshots (covered by the qa-visual-regression sister plugin).
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
--snapshot-update in CI by default | Defeats regression detection; baselines drift silently. | Updates only on developer machine + reviewed in PR (Step 2). |
| Snapshots with timestamps / random IDs | Random failures; team disables tests. | Mock clock + seed PRNG (Step 8). |
| One giant snapshot per app | Any 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 artifact | Reviewer can't see the diff; rejects blindly or rubber-stamps. | Upload snapshot_report.html artifact (Step 4). |
Limitations
References
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.