go/pkg/cli/frame_test.go
Claude 228170d610
feat(cli): TUI components, Frame AppShell, command builders (#11-15)
Issues #11-13: WithAppName for variant binaries, NewPassthrough builder
for flag.FlagSet commands, RegisterCommands test coverage with resetGlobals
helper. Fix pre-existing daemon_test.go break.

Issue #14: Rich Table with box-drawing borders (Normal, Rounded, Heavy,
Double), per-column CellStyleFn, WithMaxWidth responsive truncation.
Tree renderer with box-drawing connectors and styled nodes. Parallel
TaskTracker with braille spinners, thread-safe concurrent updates, and
non-TTY fallback. Streaming text renderer with word-wrap and channel
pattern support.

Issue #15: Frame live compositional AppShell using HLCRF regions with
Model interface, Navigate/Back content swapping, alt-screen live mode,
graceful non-TTY fallback. Built-in region components: StatusLine,
KeyHints, Breadcrumb. Zero new dependencies — pure ANSI + x/term.

68 tests, all passing with -race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:45:23 +00:00

207 lines
4.7 KiB
Go

package cli
import (
"bytes"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFrame_Good(t *testing.T) {
t.Run("static render HCF", func(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
f := NewFrame("HCF")
f.out = &bytes.Buffer{}
f.Header(StaticModel("header"))
f.Content(StaticModel("content"))
f.Footer(StaticModel("footer"))
out := f.String()
assert.Contains(t, out, "header")
assert.Contains(t, out, "content")
assert.Contains(t, out, "footer")
})
t.Run("region order preserved", func(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
f := NewFrame("HCF")
f.out = &bytes.Buffer{}
f.Header(StaticModel("AAA"))
f.Content(StaticModel("BBB"))
f.Footer(StaticModel("CCC"))
out := f.String()
posA := indexOf(out, "AAA")
posB := indexOf(out, "BBB")
posC := indexOf(out, "CCC")
assert.Less(t, posA, posB, "header before content")
assert.Less(t, posB, posC, "content before footer")
})
t.Run("navigate and back", func(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
f := NewFrame("HCF")
f.out = &bytes.Buffer{}
f.Header(StaticModel("nav"))
f.Content(StaticModel("page-1"))
f.Footer(StaticModel("hints"))
assert.Contains(t, f.String(), "page-1")
// Navigate to page 2
f.Navigate(StaticModel("page-2"))
assert.Contains(t, f.String(), "page-2")
assert.NotContains(t, f.String(), "page-1")
// Navigate to page 3
f.Navigate(StaticModel("page-3"))
assert.Contains(t, f.String(), "page-3")
// Back to page 2
ok := f.Back()
require.True(t, ok)
assert.Contains(t, f.String(), "page-2")
// Back to page 1
ok = f.Back()
require.True(t, ok)
assert.Contains(t, f.String(), "page-1")
// No more history
ok = f.Back()
assert.False(t, ok)
})
t.Run("empty regions skipped", func(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
f := NewFrame("HCF")
f.out = &bytes.Buffer{}
f.Content(StaticModel("only content"))
out := f.String()
assert.Equal(t, "only content\n", out)
})
t.Run("non-TTY run renders once", func(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
var buf bytes.Buffer
f := NewFrame("HCF")
f.out = &buf
f.Header(StaticModel("h"))
f.Content(StaticModel("c"))
f.Footer(StaticModel("f"))
f.Run() // non-TTY, should return immediately
assert.Contains(t, buf.String(), "h")
assert.Contains(t, buf.String(), "c")
assert.Contains(t, buf.String(), "f")
})
t.Run("ModelFunc adapter", func(t *testing.T) {
called := false
m := ModelFunc(func(w, h int) string {
called = true
return "dynamic"
})
out := m.View(80, 24)
assert.True(t, called)
assert.Equal(t, "dynamic", out)
})
t.Run("RunFor exits after duration", func(t *testing.T) {
var buf bytes.Buffer
f := NewFrame("C")
f.out = &buf // non-TTY → RunFor renders once and returns
f.Content(StaticModel("timed"))
start := time.Now()
f.RunFor(50 * time.Millisecond)
elapsed := time.Since(start)
assert.Less(t, elapsed, 200*time.Millisecond)
assert.Contains(t, buf.String(), "timed")
})
}
func TestFrame_Bad(t *testing.T) {
t.Run("empty frame", func(t *testing.T) {
f := NewFrame("HCF")
f.out = &bytes.Buffer{}
assert.Equal(t, "", f.String())
})
t.Run("back on empty history", func(t *testing.T) {
f := NewFrame("C")
f.out = &bytes.Buffer{}
f.Content(StaticModel("x"))
assert.False(t, f.Back())
})
t.Run("invalid variant degrades gracefully", func(t *testing.T) {
f := NewFrame("XYZ")
f.out = &bytes.Buffer{}
// No valid regions, so nothing renders
assert.Equal(t, "", f.String())
})
}
func TestStatusLine_Good(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
m := StatusLine("core dev", "18 repos", "main")
out := m.View(80, 1)
assert.Contains(t, out, "core dev")
assert.Contains(t, out, "18 repos")
assert.Contains(t, out, "main")
}
func TestKeyHints_Good(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
m := KeyHints("↑/↓ navigate", "q quit")
out := m.View(80, 1)
assert.Contains(t, out, "navigate")
assert.Contains(t, out, "quit")
}
func TestBreadcrumb_Good(t *testing.T) {
SetColorEnabled(false)
defer SetColorEnabled(true)
m := Breadcrumb("core", "dev", "health")
out := m.View(80, 1)
assert.Contains(t, out, "core")
assert.Contains(t, out, "dev")
assert.Contains(t, out, "health")
assert.Contains(t, out, ">")
}
func TestStaticModel_Good(t *testing.T) {
m := StaticModel("hello")
assert.Equal(t, "hello", m.View(80, 24))
}
// indexOf returns the position of substr in s, or -1 if not found.
func indexOf(s, substr string) int {
for i := range len(s) - len(substr) + 1 {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}