termtap/internal/tui/view_split_panes_style_test.go
Hayden Hargreaves 8c399b6754 feat: search functionality.
Bit rushed, but its on the site. Going to add a real larger prompt about
it.
2026-05-01 14:51:00 -07:00

283 lines
8.2 KiB
Go

package tui
import (
"strings"
"testing"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/google/uuid"
"termtap.dev/internal/model"
)
func TestFormatDuration(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in time.Duration
want string
}{
{name: "pending zero", in: 0, want: "PENDING"},
{name: "microseconds", in: 750 * time.Microsecond, want: "750us"},
{name: "milliseconds", in: 20 * time.Millisecond, want: "20ms"},
{name: "seconds", in: 11 * time.Second, want: "11.00s"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := formatDuration(tt.in); got != tt.want {
t.Fatalf("formatDuration(%v) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestTruncate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
s string
max int
want string
}{
{name: "short unchanged", s: "abc", max: 3, want: "abc"},
{name: "max small no ellipsis", s: "abcdef", max: 3, want: "abc"},
{name: "ellipsis", s: "abcdef", max: 5, want: "ab..."},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := truncate(tt.s, tt.max); got != tt.want {
t.Fatalf("truncate(%q,%d) = %q, want %q", tt.s, tt.max, got, tt.want)
}
})
}
}
func TestClampRendered(t *testing.T) {
t.Parallel()
if got := clampRendered("abcdef", 0); got != "" {
t.Fatalf("clampRendered max=0 = %q, want empty", got)
}
if got := clampRendered("abc", 10); got != "abc" {
t.Fatalf("clampRendered no truncation = %q, want %q", got, "abc")
}
if got := clampRendered("abcdef", 4); !strings.Contains(got, "...") {
t.Fatalf("clampRendered truncation should include ellipsis, got %q", got)
}
}
func TestGetEventColor(t *testing.T) {
t.Parallel()
theme := newTheme()
tests := []struct {
name string
typ model.EventType
want string
}{
{name: "session", typ: model.EventTypeSessionStarted, want: theme.EventSession.Render("x")},
{name: "proxy", typ: model.EventTypeProxyStarted, want: theme.EventProxy.Render("x")},
{name: "request in flight", typ: model.EventTypeRequestStarted, want: theme.EventRequestInFlight.Render("x")},
{name: "request success", typ: model.EventTypeRequestFinished, want: theme.EventSuccess.Render("x")},
{name: "warn", typ: model.EventTypeWarn, want: theme.EventWarn.Render("x")},
{name: "error", typ: model.EventTypeRequestFailed, want: theme.EventError.Render("x")},
{name: "fatal", typ: model.EventTypeFatal, want: theme.EventFatal.Render("x")},
{name: "default", typ: model.EventType("UnknownType"), want: theme.EventDefault.Render("x")},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := getEventColor(theme, tt.typ).Render("x")
if got != tt.want {
t.Fatalf("unexpected style for %s", tt.typ)
}
})
}
}
func TestViewAndPaneStructure(t *testing.T) {
t.Parallel()
m := NewModel(make(chan model.Event), Controls{})
t.Run("view returns raw pane when unset size", func(t *testing.T) {
t.Parallel()
got := m.View()
if got != m.renderAppPane() {
t.Fatal("View should return raw pane when width/height are unset")
}
})
t.Run("renderAppPane line count matches height", func(t *testing.T) {
t.Parallel()
m2 := m
m2.width = 80
m2.height = 12
got := m2.renderAppPane()
if got == "height of request and details did not match" || got == "height of screen does not match terminal height" {
t.Fatalf("unexpected renderAppPane invariant error: %q", got)
}
if lines := strings.Count(got, "\n") + 1; lines != m2.height {
t.Fatalf("line count = %d, want %d", lines, m2.height)
}
})
t.Run("renderAppPane supports toggles", func(t *testing.T) {
t.Parallel()
m2 := m
m2.width = 90
m2.height = 14
m2.showEvents = true
m2.showStd = true
m2.showSearch = true
got := m2.renderAppPane()
if got == "height of request and details did not match" || got == "height of screen does not match terminal height" {
t.Fatalf("unexpected renderAppPane invariant error with toggles: %q", got)
}
})
t.Run("view applies configured terminal height", func(t *testing.T) {
t.Parallel()
m2 := m
m2.width = 70
m2.height = 10
got := m2.View()
if lines := strings.Count(got, "\n") + 1; lines < m2.height {
t.Fatalf("View line count = %d, want at least %d", lines, m2.height)
}
})
}
func TestPaneRenderersAndStatusBar(t *testing.T) {
t.Parallel()
m := NewModel(make(chan model.Event), Controls{})
m.width = 100
m.height = 12
m.requests = []model.Request{
{ID: uuid.New(), Method: "GET", Host: "a", URL: "/a", Status: 200, Duration: 5 * time.Millisecond},
{ID: uuid.New(), Method: "POST", Host: "b", URL: "/b", Status: 500, Duration: 10 * time.Millisecond, Failed: true},
}
m.events = []model.Event{
{Type: model.EventTypeWarn, Body: "warn"},
{Type: model.EventTypeProcessStdout, Body: "out"},
{Type: model.EventTypeProcessStderr, Body: "err"},
}
status := m.renderStatusBar(100)
if !strings.Contains(status, "2 reqs") || !strings.Contains(status, "1 err") {
t.Fatalf("status bar missing expected counters: %q", status)
}
search := m.renderSearchPane(20, 3)
if len(search) != 3 {
t.Fatalf("search pane len = %d, want 3", len(search))
}
for i, line := range search {
if lipgloss.Width(line) != 20 {
t.Fatalf("search pane line %d width = %d, want %d", i, lipgloss.Width(line), 20)
}
}
requests := m.renderRequestPane(50, 4)
if len(requests) != 4 {
t.Fatalf("request pane len = %d, want 4", len(requests))
}
details := m.renderDetailsPane(30, 4)
if len(details) != 4 {
t.Fatalf("details pane len = %d, want 4", len(details))
}
events := m.renderEventsPane(60, 4)
if len(events) != 4 {
t.Fatalf("events pane len = %d, want 4", len(events))
}
if strings.Contains(strings.Join(events, "\n"), "out") || strings.Contains(strings.Join(events, "\n"), "err") {
t.Fatal("events pane should filter stdout/stderr events")
}
std := m.renderStdPane(60, 4)
if len(std) != 4 {
t.Fatalf("std pane len = %d, want 4", len(std))
}
joined := strings.Join(std, "\n")
if !strings.Contains(joined, "out") || !strings.Contains(joined, "err") {
t.Fatal("std pane should include stdout/stderr logs")
}
}
func TestRequestSearchFiltering(t *testing.T) {
t.Parallel()
m := NewModel(make(chan model.Event), Controls{})
m.requests = []model.Request{
{ID: uuid.New(), Method: "GET", Host: "api.example.com", URL: "/v1/users", Status: 200},
{ID: uuid.New(), Method: "POST", Host: "api.example.com", URL: "/v1/login", Status: 401},
{ID: uuid.New(), Method: "GET", Host: "cdn.example.com", URL: "/asset.js", Status: 304},
{ID: uuid.New(), Method: "DELETE", Host: "api.example.com", URL: "/v1/users/42", Status: 500},
}
m.searchQuery = "api.example.com method:get status:2xx"
idx := m.filteredRequestIndices()
if len(idx) != 1 {
t.Fatalf("filteredRequestIndices len = %d, want 1", len(idx))
}
if got := m.requests[idx[0]].URL; got != "/v1/users" {
t.Fatalf("filtered request URL = %q, want %q", got, "/v1/users")
}
m.searchQuery = "status:5xx method:delete"
idx = m.filteredRequestIndices()
if len(idx) != 1 {
t.Fatalf("filteredRequestIndices len = %d, want 1", len(idx))
}
if got := m.requests[idx[0]].Status; got != 500 {
t.Fatalf("filtered request status = %d, want 500", got)
}
m.searchQuery = "status:404"
idx = m.filteredRequestIndices()
if len(idx) != 0 {
t.Fatalf("filteredRequestIndices len = %d, want 0", len(idx))
}
}
func TestRenderEventsPane_ErrorAndPIDBranches(t *testing.T) {
t.Parallel()
m := NewModel(make(chan model.Event), Controls{})
m.events = []model.Event{
{Type: model.EventTypeWarn, Body: "old"},
{Type: model.EventTypeRequestFailed, Body: "failed body", PID: 123, Time: time.Now()},
{Type: model.EventTypeFatal, Body: "fatal body", Time: time.Now()},
}
lines := m.renderEventsPane(60, 3)
if len(lines) != 3 {
t.Fatalf("events pane len = %d, want 3", len(lines))
}
joined := strings.Join(lines, "\n")
if !strings.Contains(joined, "123") {
t.Fatalf("expected PID to appear in events pane, got: %q", joined)
}
if !strings.Contains(joined, "failed body") {
t.Fatalf("expected failed body to appear in events pane, got: %q", joined)
}
if !strings.Contains(joined, "fatal body") {
t.Fatalf("expected fatal body to appear in events pane, got: %q", joined)
}
}