package app
import (
"fmt"
"strings"
"sync"
"testing"
"time"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"github.com/creack/pty"
xterm "github.com/gitpod-io/xterm-go"
"github.com/samber/lo"
)
// with more hosts than the terminal has rows, typing and moving must still
// show on screen: an overlong view loses its top to the renderer, so the
// keys land in the model while only the spinners visibly change
func TestLongTablesKeepTheFilterAndCursorOnScreen(t *testing.T) {
const hosts, rows, cols = 40, 24, 80
f := textinput.New()
f.Prompt = "› "
f.Focus()
f.SetWidth(cols - 8)
m := pickerModel{width: cols, filter: f, th: newTheme(true), spinner: spinner.New(spinner.WithSpinner(spinner.Dot))}
m.rows = []hostRow{{name: "selfish", local: true}}
for i := range hosts {
m.rows = append(m.rows, hostRow{name: fmt.Sprintf("host%03d", i), probing: true, started: time.Now()})
}
m.probes = make(chan probeResult, hosts)
m.move(0)
master, tty, err := pty.Open()
if err != nil {
t.Fatal(err)
}
defer master.Close()
defer tty.Close()
if err := pty.Setsize(tty, &pty.Winsize{Rows: rows, Cols: cols}); err != nil {
t.Fatal(err)
}
vt := xterm.New(xterm.WithCols(cols), xterm.WithRows(rows))
vt.OnData(func(s string) { _, _ = master.Write([]byte(s)) }) // answer the program's queries
var mu sync.Mutex
go func() {
buf := make([]byte, 64<<10)
for {
n, err := master.Read(buf)
mu.Lock()
_, _ = vt.Write(buf[:n])
mu.Unlock()
if err != nil {
return
}
}
}()
screen := func() string { mu.Lock(); defer mu.Unlock(); return vt.String() }
shows := func(want []string) bool {
s := screen()
return lo.EveryBy(want, func(w string) bool { return strings.Contains(s, w) })
}
waitFor := func(want ...string) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
for !shows(want) {
if time.Now().After(deadline) {
t.Fatalf("the screen never showed %q:\n%s", want, screen())
}
time.Sleep(20 * time.Millisecond)
}
}
p := tea.NewProgram(m, tea.WithInput(tty), tea.WithOutput(tty))
done := make(chan error, 1)
go func() { _, err := p.Run(); done <- err }()
defer func() { p.Quit(); <-done }()
waitFor("│ ›", "› selfish")
_, _ = master.Write([]byte("hos\x1b[B\x1b[B\x1b[B")) // selfish drops out, so the cursor lands on the fourth host
waitFor("│ › hos", "› host003")
// a filter nothing matches shrinks the view; the renderer has to clear
// the taller frame above it rather than leave its top behind
_, _ = master.Write([]byte("qqq"))
waitFor("nothing matches")
if n := strings.Count(screen(), "tobi"); n != 1 {
t.Fatalf("the shrunk view left %d headers on screen:\n%s", n, screen())
}
_, _ = master.Write([]byte("\x1b")) // esc clears the filter and grows it back
waitFor("› selfish")
if n := strings.Count(screen(), "tobi"); n != 1 {
t.Fatalf("the regrown view left %d headers on screen:\n%s", n, screen())
}
}