make test # all goldens, ~0.6s
make snap-update # regenerate testdata/*.golden after intentional change
make preview # page through every golden in less -R
make preview ARGS=welcome # filter by substring
make preview ARGS="bundles xl" # multi-arg = AND filterWhy not teatest's byte-stream
teatest's byte capture returns the full ANSI escape stream including alt-screen sequences — useless for diffing. The test suite bypasses tea.Program entirely:
New(theme)→ rootModel- Drive via direct
m.Update(...)calls with synthetic key messages - Call
m.View()to capture the rendered string - Diff against
testdata/<name>.golden
The cmd returned by Update is resolved exactly once (via cmd()) to surface synchronous transitions. Async cmds (timers, ticks) don't fire — fine for our case because every screen renders meaningfully on first paint.
Update flag
Prefer -update over -update-snap (the flag.Lookup("update") lookup catches teatest's reserved flag).
go test ./internal/tui -update -run TestSnapshot_WelcomeCoverage matrix
Goldens cover 6 widths × 4 themes for every screen — xs / sm / md / lg / xl / xxl × lfg / dracula / catppuccin / colorblind. Adding a screen means adding a corresponding TestSnapshot_<Name> in screens_test.go that loops over the matrix.
Mock data, never the host
tui.New(theme) injects mockProgressRunner for the install step; CLI startup passes the real installer.Run. Don't conflate the two paths — wiring real subprocesses into snapshot tests would make goldens non-deterministic and slow.
The bundle/tool data the picker sees in tests is a deterministic mock list, not the host system. Tool.Installed and Tool.Version are stubbed; live registry probes are no-ops.
When goldens diverge
make test shows the diff. If intentional:
make snap-update
git add internal/tui/testdata/
git commit -m "snap: update goldens for <change>"Reviewers can eyeball the golden diff in the PR — that's the whole point of checking them in.