Use when debugging TUI layout issues, writing regression tests for rendering problems, or analyzing View() output structure with RenderingAnalyzer.
Use this when: Debugging TUI layout issues, writing regression tests for rendering problems, or analyzing View() output structure.
For any rendering bug, use the test apparatus to:
// 1. Get the output
output := m.View()
// 2. Create an analyzer
analyzer := tui.NewRenderingAnalyzer(t, output)
// 3. Verify structure
analyzer.AssertLineSeparation("routing status", "queue indicator")
analyzer.AssertNoHorizontalConcat("status", "queue:")
analyzer.AssertContains("expected text")
// 4. Debug if needed
analyzer.Dump() // See line-by-line output| Method | Use For |
|---|---|
AssertLineSeparation(text1, text2) | Prevent overlap bugs — verify items are on different lines |
AssertNoHorizontalConcat(text1, text2) | Detect horizontal concatenation when should be vertical |
AssertContains(text) | Verify expected content is in output |
AssertLineCount(n) | Verify exact line count |
AssertStructure(elements...) | Verify output contains expected elements |
Dump() | Print full output with line numbers for debugging |
StrippedLines() | Get output with ANSI codes removed |
Bug: Queue indicator appeared on same terminal line as routing status.
Test that catches it:
func TestQueueIndicatorNotOverlapped(t *testing.T) {
m := newTestModel()
m.mode = ModeAwaitingLLM
m.transcript.AppendLive("↳ resolved: view")
output := m.View()
analyzer := tui.NewRenderingAnalyzer(t, output)
// This test fails if the bug reoccurs
analyzer.AssertLineSeparation("resolved", "⏳")
analyzer.AssertNoHorizontalConcat("resolved", "queue:")
}Symptom: "last content framework: awaiting" on one line
Test:
analyzer.AssertLineSeparation("content above", "framework: awaiting")Root Cause: JoinVertical padding or double newlines causing cursor misalignment.
Symptom: Indicator and prompt on same line when should be separate
Test:
analyzer.AssertLineSeparation("⏳ queue", "↳ prompt")
analyzer.AssertLineCount(expectedLines)Symptom: Weird characters in output or misaligned styling
Test:
analyzer.StrippedLines() // Analyze without ANSI codes
analyzer.AssertContains("visible text") // Works with ANSIExample: "Queue indicator appears on same line as routing status"
func TestBugName(t *testing.T) {
m := newTestModel()
m.mode = ModeAwaitingLLM
m.transcript.AppendLive("↳ status")
m.inputQueue = append(m.inputQueue, "queued input")
output := m.View()
analyzer := tui.NewRenderingAnalyzer(t, output)// These should FAIL if bug exists, PASS if fixed
analyzer.AssertLineSeparation("status", "queue:")
analyzer.AssertNoHorizontalConcat("status", "queue:")
}analyzer.Dump()Shows:
Example output:
=== Rendered Output ===
Line 0 (bytes= 35): "↳ resolved: deterministic · 1.00"
Line 1 (bytes=240): "─────────────────────────────…"
Line 2 (bytes= 60): "⏳ ⠋ thinking… · queue: 0"
Line 3 (bytes= 14): "↳ test input"If bug only manifests at certain widths:
for _, width := range []int{80, 120, 200} {
t.Run(fmt.Sprintf("Width%d", width), func(t *testing.T) {
m.SetWidth(width)
output := m.View()
analyzer := tui.NewRenderingAnalyzer(t, output)
analyzer.AssertLineSeparation("text1", "text2")
})
}rendering_test_utils.go — RenderingAnalyzer implementationrendering_regression_test.go — Example tests (copy these patterns)docs/tui/rendering-tests.md — Full documentation❌ Using string Contains for layout — use AssertLineSeparation() instead
❌ Forgetting about ANSI codes — analyzer handles them, but be aware of byte vs visible length
❌ Testing implementation, not behavior — test what users see
❌ Tests too specific — focus on critical structure, not exact positions
The mistake: Testing View() output in isolation and declaring bugs fixed based on test passes.
When this fails: Bugs involving:
User reported: "Queue indicator appears at end of log lines"
2026/05/29 05:40:41 INFO metamode.oracle.event ... ⏳ ⠏ running…Isolated test (❌ can't catch it):
func TestQueueIndicator(t *testing.T) {
output := m.View()
analyzer := tui.NewRenderingAnalyzer(t, output)
analyzer.AssertLineSeparation("queue", "status") // ✅ Passes!
// But this doesn't test: slog writes to stderr while View() renders
}Integration test (✅ catches it):
func TestSlogDoesNotMixWithQueueIndicator(t *testing.T) {
// Capture actual stderr output
var stderrBuf strings.Builder
oldDefault := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&stderrBuf, nil)))
defer slog.SetDefault(oldDefault)
// Simulate concurrent logging + rendering (where the bug lives)
go func() {
for i := 0; i < 50; i++ {
slog.Info("metamode.oracle.event", "type", "system")
time.Sleep(time.Microsecond)
}
}()
for i := 0; i < 50; i++ {
m.View()
time.Sleep(time.Microsecond)
}
// Assert on ACTUAL output that reaches the terminal
stderrLines := strings.Split(stderrBuf.String(), "\n")
for _, line := range stderrLines {
if strings.Contains(line, "INFO") && strings.Contains(line, "⏳") {
t.Fatalf("queue indicator mixed into log: %q", line)
}
}
}When to use isolated tests:
When you must add integration tests:
If the user's symptom involves:
Action: Don't just run the isolated test suite. Build a test that:
Before declaring a bug fixed:
This is non-negotiable. A passing test that would pass even without your fix is not testing the fix.
# All rendering tests
go test ./internal/tui -run Rendering -v
# Specific regression test
go test ./internal/tui -run TestQueueIndicatorNotOverlapped -v
# Show full output
go test ./internal/tui -run TestName -vThe bug was caused by lipgloss.JoinVertical() applying width-based padding that caused unintended horizontal alignment of multi-line parts.
Fix: Use simple string concatenation instead.
// ❌ Before (causes padding issues)
return lipgloss.JoinVertical(lipgloss.Left, parts...)
// ✅ After (preserves structure)
var output strings.Builder
for _, part := range parts {
trimmed := strings.TrimRight(part, "\n")
if trimmed != "" {
if output.Len() > 0 {
output.WriteString("\n")
}
output.WriteString(trimmed)
}
}
return output.String()This testing apparatus prevents this class of bugs from recurring.
1f4abf0
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.