Use when a terminal UI (TUI) app needs tests that catch layout regressions. Captures rendered terminal frames as SVG or text snapshots and diffs them on each run: pytest-textual-snapshot for Textual, insta for Ratatui, teatest for Bubble Tea, ink-testing-library for Ink.
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is testland/tui-snapshot-tester
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-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.
For headless CLIs that emit text only, prefer
bats-testing +
cli-output-conventions;
TUI snapshots are only needed for layout-rich UIs.
Per txt:
pip install pytest-textual-snapshotThis pulls Textual + the snap_compare pytest fixture.
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-updateif you're happy with how the output looks on the left hand side of the snapshot report."
Workflow:
pytest. It fails; emits an HTML report at
snapshot_report.html.pytest --snapshot-update
to accept it.__snapshots__/ directory.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.
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.
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/.
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 ./...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.
Snapshots fail randomly if any of these vary:
freezegun;
Ratatui: pass now as state; Ink: use vi.useFakeTimers()).LC_ALL=C in CI.Without determinism, snapshot tests become noise → team disables.
Treat snapshot diffs like test diffs in code review:
--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-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). |
terminal_size=).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.testland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.