tui-test
Specialized guidance for testing TUI applications with Bubbletea, including golden file testing, component testing, and integration testing with teatest.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Specialized guidance for testing TUI applications with Bubbletea, including golden file testing, component testing, and integration testing with teatest.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Best practices for working with Go codebases. Use when writing, debugging, or exploring Go code, including reading dependency sources and documentation.
Autonomous orchestration loop for hive hc tasks. The high-capability manager model reads pending tasks, delegates implementation to cheaper worker sub-agents, critically reviews their output, and commits only code that passes quality checks. Runs to completion without stopping to ask questions.
Post-implementation design re-evaluation. After a feature reaches a working state through iteration, step back and re-examine what actually got built: are the data structures and algorithms the right fit for the access patterns that emerged, can code paths that grew through iteration be consolidated, and what iteration residue (tests for abandoned designs, dead flags/scaffolding, debug logging) should be deleted. Produces a tiered proposal report and applies only approved changes. Use when the user says "rethink this", "step back and re-evaluate", "is this the right design/data structure", "apply CS fundamentals", or wants a design pass on a working feature branch before PR. Not for bug-hunting (/review) or expression-level polish (/simplify) — this questions the design those skills preserve.
Orchestrate parallel claude and codex CLI agents through tmux to deliver a feature end-to-end. The orchestrator delegates planning, work, and review to spawned agents in tmux windows; it does NOT write or edit code itself. User-invoked only.
Open the current branch's diff (or a specific PR) in Plannotator's browser-based code review UI and act on the returned feedback. Use when the user says "review my changes in plannotator", "open the diff for review", "review this PR in plannotator", "let me annotate the diff", or returns to a session to gate code the agent produced.
Single-pass code review of the current branch (or a diff) that routes the changes to relevant concerns, dispatches fresh-context reviewer sub-agents, verifies findings to strip false positives, and reports a ranked, evidence-backed review. Use when the user asks to review local changes, a branch, or a PR before it goes to humans.
| name | tui-test |
| description | Specialized guidance for testing TUI applications with Bubbletea, including golden file testing, component testing, and integration testing with teatest. |
Specialized guidance for testing Terminal User Interface applications with Bubbletea, focusing on golden file testing, component testing, and integration testing.
Activate when:
Primary method for testing TUI rendering:
package myapp
import (
"testing"
"github.com/charmbracelet/x/exp/golden"
)
func TestRender(t *testing.T) {
m := Model{
title: "Test App",
width: 80,
items: []string{"one", "two", "three"},
}
output := m.View()
// Compares with testdata/TestRender.golden
golden.RequireEqual(t, output)
}
// Run tests: go test
// Update golden files: go test -update
Directory structure:
myapp/
├── component.go
├── component_test.go
└── testdata/
├── TestRender.golden
├── TestRenderEmpty.golden
└── TestRenderWithScroll.golden
Best practices:
Test state transitions directly:
func TestUpdateNavigation(t *testing.T) {
tests := []struct {
name string
initialModel Model
msg tea.Msg
wantCursor int
wantCmd bool
}{
{
name: "down arrow increases cursor",
initialModel: Model{cursor: 0, items: []string{"a", "b"}},
msg: tea.KeyPressMsg{Type: tea.KeyDown},
wantCursor: 1,
wantCmd: false,
},
{
name: "down at bottom wraps to top",
initialModel: Model{cursor: 1, items: []string{"a", "b"}},
msg: tea.KeyPressMsg{Type: tea.KeyDown},
wantCursor: 0,
wantCmd: false,
},
{
name: "enter selects item",
initialModel: Model{cursor: 0, items: []string{"a", "b"}},
msg: tea.KeyPressMsg{Type: tea.KeyEnter},
wantCursor: 0,
wantCmd: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := tt.initialModel
newModel, cmd := m.Update(tt.msg)
got := newModel.(Model)
if got.cursor != tt.wantCursor {
t.Errorf("cursor = %v, want %v", got.cursor, tt.wantCursor)
}
if (cmd != nil) != tt.wantCmd {
t.Errorf("cmd = %v, wantCmd = %v", cmd != nil, tt.wantCmd)
}
})
}
}
func TestTextInputComponent(t *testing.T) {
ti := textinput.New()
ti.SetValue("initial")
ti.Focus()
// Test typing
ti, _ = ti.Update(tea.KeyPressMsg{
Runes: []rune{'x'},
Type: tea.KeyRunes,
})
if ti.Value() != "initialx" {
t.Errorf("value = %q, want %q", ti.Value(), "initialx")
}
// Test backspace
ti, _ = ti.Update(tea.KeyPressMsg{Type: tea.KeyBackspace})
if ti.Value() != "initial" {
t.Errorf("value = %q, want %q", ti.Value(), "initial")
}
// Test view contains expected text
view := ti.View()
if !strings.Contains(view, "initial") {
t.Error("view doesn't contain expected text")
}
}
import (
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/exp/teatest"
)
func TestFullProgram(t *testing.T) {
m := NewModel()
tm := teatest.NewTestModel(
t, m,
teatest.WithInitialTermSize(80, 24),
)
t.Cleanup(func() {
if err := tm.Quit(); err != nil {
t.Fatal(err)
}
})
// Wait for initialization
time.Sleep(100 * time.Millisecond)
// Simulate user input
tm.Type("hello world")
tm.Send(tea.KeyPressMsg{Type: tea.KeyEnter})
// Wait for specific output
teatest.WaitFor(
t,
tm.Output(),
func(bts []byte) bool {
return bytes.Contains(bts, []byte("Success"))
},
teatest.WithCheckInterval(50*time.Millisecond),
teatest.WithDuration(3*time.Second),
)
// Get final output
output := tm.FinalOutput(t)
golden.RequireEqual(t, output)
// Verify final model state
fm := tm.FinalModel(t)
finalModel, ok := fm.(Model)
if !ok {
t.Fatal("wrong model type")
}
if finalModel.state != stateComplete {
t.Errorf("state = %v, want %v", finalModel.state, stateComplete)
}
}
func TestProgramWithMockIO(t *testing.T) {
var output bytes.Buffer
var input bytes.Buffer
// Pre-fill input
input.WriteString("test input\n")
input.WriteByte('q')
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
m := NewModel()
p := tea.NewProgram(m,
tea.WithContext(ctx),
tea.WithInput(&input),
tea.WithOutput(&output),
)
finalModel, err := p.Run()
if err != nil {
t.Fatal(err)
}
// Verify output
if output.Len() == 0 {
t.Error("no output produced")
}
// Verify final state
fm, ok := finalModel.(Model)
if !ok {
t.Fatal("wrong model type")
}
if fm.value != "test input" {
t.Errorf("value = %q, want %q", fm.value, "test input")
}
}
func keyPress(key rune) tea.Msg {
return tea.KeyPressMsg{
Runes: []rune{key},
Type: tea.KeyRunes,
}
}
func keyPressString(s string) tea.Msg {
return tea.KeyPressMsg{
Runes: []rune(s),
Type: tea.KeyRunes,
}
}
func keyDown() tea.Msg {
return tea.KeyPressMsg{Type: tea.KeyDown}
}
func keyEnter() tea.Msg {
return tea.KeyPressMsg{Type: tea.KeyEnter}
}
func sendString(m Model, s string) Model {
for _, r := range s {
m, _ = m.Update(keyPress(r))
}
return m
}
import "github.com/charmbracelet/x/ansi"
func stripANSI(s string) string {
s = ansi.Strip(s)
lines := strings.Split(s, "\n")
var result []string
for _, line := range lines {
trimmed := strings.TrimRight(line, " ")
if trimmed != "" {
result = append(result, trimmed)
}
}
return strings.Join(result, "\n")
}
func windowSize(w, h int) tea.WindowSizeMsg {
return tea.WindowSizeMsg{Width: w, Height: h}
}
func TestView(t *testing.T) {
tests := []struct {
name string
setup func(Model) Model
wantView string
}{
{
name: "empty state",
wantView: heredoc.Doc(`
> No items
>
`),
},
{
name: "with items",
setup: func(m Model) Model {
m.items = []string{"one", "two"}
return m
},
wantView: heredoc.Doc(`
> one
> two
>
`),
},
{
name: "with cursor",
setup: func(m Model) Model {
m.items = []string{"one", "two"}
m.cursor = 1
return m
},
wantView: heredoc.Doc(`
> one
> ❯ two
>
`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewModel()
if tt.setup != nil {
m = tt.setup(m)
}
got := stripANSI(m.View())
want := stripANSI(tt.wantView)
if got != want {
t.Errorf("View() =\n%v\nwant\n%v", got, want)
}
})
}
}
Unit Tests:
View Tests:
Integration Tests:
Component Tests:
func TestSequentialUpdates(t *testing.T) {
m := NewModel()
// Apply sequence of updates
updates := []tea.Msg{
tea.KeyPressMsg{Type: tea.KeyDown},
tea.KeyPressMsg{Type: tea.KeyDown},
tea.KeyPressMsg{Type: tea.KeyEnter},
}
for _, msg := range updates {
m, _ = m.(Model).Update(msg)
}
if m.(Model).cursor != 2 {
t.Errorf("cursor = %v, want 2", m.(Model).cursor)
}
}
func TestCommandExecution(t *testing.T) {
m := NewModel()
_, cmd := m.Update(tea.KeyPressMsg{Type: tea.KeyEnter})
if cmd == nil {
t.Fatal("expected command, got nil")
}
// Execute command
msg := cmd()
// Verify message type
if _, ok := msg.(dataLoadedMsg); !ok {
t.Errorf("expected dataLoadedMsg, got %T", msg)
}
}
func TestFocusHandling(t *testing.T) {
m := NewModel()
if m.Focused() {
t.Error("should not be focused initially")
}
m.Focus()
if !m.Focused() {
t.Error("should be focused after Focus()")
}
// Should handle input when focused
m, _ = m.Update(keyPress('a'))
if m.value != "a" {
t.Error("should accept input when focused")
}
m.Blur()
if m.Focused() {
t.Error("should not be focused after Blur()")
}
// Should ignore input when not focused
originalValue := m.value
m, _ = m.Update(keyPress('b'))
if m.value != originalValue {
t.Error("should ignore input when not focused")
}
}
Golden file mismatch:
go test -update if correctFlaky tests:
Missing output:
Test behavior, not implementation
Use descriptive test names
Keep tests fast
Make tests deterministic
Test error paths
Use table-driven tests