698 lines
21 KiB
Go
698 lines
21 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/bubbles/list"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/mirivlad/sshkeeper/internal/model"
|
|
)
|
|
|
|
func TestServerListViewUsesDashboardLayout(t *testing.T) {
|
|
now := time.Date(2026, 5, 28, 1, 50, 0, 0, time.UTC)
|
|
m := New([]*model.Server{
|
|
{
|
|
Alias: "mail.kp",
|
|
DisplayName: "Mail",
|
|
Host: "mail.example.org",
|
|
Port: 222,
|
|
User: "mirivlad",
|
|
AuthMethod: model.AuthPassword,
|
|
GroupName: "KP",
|
|
LastTestStatus: model.TestOK,
|
|
LastTestAt: &now,
|
|
},
|
|
{
|
|
Alias: "mirv.top",
|
|
Host: "mirv.top",
|
|
Port: 22,
|
|
User: "root",
|
|
AuthMethod: model.AuthKey,
|
|
LastTestStatus: model.TestUnknown,
|
|
},
|
|
})
|
|
m.width = 100
|
|
m.height = 30
|
|
m.list.SetSize(100, 24)
|
|
|
|
view := m.View()
|
|
for _, want := range []string{
|
|
"sshkeeper",
|
|
"2 servers",
|
|
"Vault",
|
|
"NAME",
|
|
"TARGET",
|
|
"AUTH",
|
|
"GROUP",
|
|
"STATUS",
|
|
"Mail",
|
|
"mail.kp",
|
|
"mirivlad@mail.example.org:222",
|
|
"KP",
|
|
"OK",
|
|
"Selected",
|
|
"Host: mail.example.org",
|
|
"Alias: mail.kp",
|
|
"Display Name: Mail",
|
|
"Port: 222",
|
|
"Enter",
|
|
"connect",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected list view to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
if strings.Contains(view, "Profiles managed locally") {
|
|
t.Fatalf("expected compact status header instead of README text\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestServerListViewKeepsDetailsVisibleWithManyServers(t *testing.T) {
|
|
servers := make([]*model.Server, 45)
|
|
for i := range servers {
|
|
servers[i] = &model.Server{
|
|
Alias: fmt.Sprintf("server-%02d", i+1),
|
|
DisplayName: fmt.Sprintf("Server %02d", i+1),
|
|
Host: fmt.Sprintf("host-%02d.example.org", i+1),
|
|
Port: 22,
|
|
User: "mirivlad",
|
|
AuthMethod: model.AuthKey,
|
|
LastTestStatus: model.TestUnknown,
|
|
}
|
|
}
|
|
|
|
m := New(servers)
|
|
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 18})
|
|
model := updated.(*tuiModel)
|
|
|
|
view := model.View()
|
|
if !strings.Contains(view, "Server 01") {
|
|
t.Fatalf("expected first selected server to be visible:\n%s", view)
|
|
}
|
|
if !strings.Contains(view, "Selected") {
|
|
t.Fatalf("expected selected server details to remain visible:\n%s", view)
|
|
}
|
|
if !strings.Contains(view, "Enter") || !strings.Contains(view, "connect") {
|
|
t.Fatalf("expected footer to remain visible:\n%s", view)
|
|
}
|
|
if count := strings.Count(view, "server-"); count >= len(servers) {
|
|
t.Fatalf("expected bounded row rendering, rendered %d server aliases", count)
|
|
}
|
|
}
|
|
|
|
func TestServerListHelpWrapsOnNarrowTerminal(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.width = 72
|
|
m.height = 24
|
|
|
|
view := m.View()
|
|
for _, line := range strings.Split(view, "\n") {
|
|
if strings.Contains(line, "Enter") && strings.Contains(line, "connect") && lipgloss.Width(line) > 72 {
|
|
t.Fatalf("expected help line to be bounded, got width %d: %q\nview:\n%s", lipgloss.Width(line), line, view)
|
|
}
|
|
}
|
|
for _, want := range []string{"Ctrl+R", "run tpl", "Ctrl+P", "tpl mgr"} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected help to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
if strings.Contains(view, "Shift+Enter") {
|
|
t.Fatalf("expected help to omit Shift+Enter\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestServerListHelpWrapsSelectionAndResultHints(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.width = 90
|
|
lines := wrapHelpItems(m.listHelpItems(2, true), m.width-2)
|
|
|
|
if len(lines) < 2 {
|
|
t.Fatalf("expected wrapped help, got %#v", lines)
|
|
}
|
|
for _, line := range lines {
|
|
plain := plainHelpLine(line)
|
|
if len(plain) > m.width-2 {
|
|
t.Fatalf("help line too long: len=%d line=%q", len(plain), plain)
|
|
}
|
|
}
|
|
var plainLines []string
|
|
for _, line := range lines {
|
|
plainLines = append(plainLines, plainHelpLine(line))
|
|
}
|
|
joined := strings.Join(plainLines, "\n")
|
|
for _, want := range []string{"Ins: select (2 selected)", "Esc: clear result", "Ctrl+P: tpl mgr", "Ctrl+Q: quit"} {
|
|
if !strings.Contains(joined, want) {
|
|
t.Fatalf("expected wrapped help to contain %q\nlines:%#v", want, lines)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServerListFooterUsesColonFormatAndColoredHotkeys(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
{Alias: "two", Host: "two.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.width = 90
|
|
m.height = 30
|
|
m.selected["one"] = true
|
|
|
|
view := m.View()
|
|
for _, want := range []string{"Ins", ": select (1 selected)", "Ctrl+A", ": add"} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected footer to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
if hotkeyStyle.GetForeground() == nil {
|
|
t.Fatal("expected hotkey style to define a foreground color")
|
|
}
|
|
lines := strings.Split(view, "\n")
|
|
if got := len(lines); got != m.height {
|
|
t.Fatalf("expected footer to be pinned to bottom with %d lines, got %d\nview:\n%s", m.height, got, view)
|
|
}
|
|
if !strings.Contains(lines[len(lines)-1], "Ctrl+Q") {
|
|
t.Fatalf("expected final footer line at terminal bottom, got %q\nview:\n%s", lines[len(lines)-1], view)
|
|
}
|
|
}
|
|
|
|
func TestVisibleServerRangeKeepsSelectionInsideWindow(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
total int
|
|
selected int
|
|
available int
|
|
wantStart int
|
|
wantEnd int
|
|
}{
|
|
{name: "first page", total: 40, selected: 0, available: 10, wantStart: 0, wantEnd: 10},
|
|
{name: "middle page", total: 40, selected: 20, available: 10, wantStart: 11, wantEnd: 21},
|
|
{name: "last page", total: 40, selected: 39, available: 10, wantStart: 30, wantEnd: 40},
|
|
{name: "all fit", total: 5, selected: 3, available: 10, wantStart: 0, wantEnd: 5},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
start, end := visibleServerRange(tt.total, tt.selected, tt.available)
|
|
if start != tt.wantStart || end != tt.wantEnd {
|
|
t.Fatalf("visibleServerRange() = %d, %d; want %d, %d", start, end, tt.wantStart, tt.wantEnd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServerListViewScrollsWithSelection(t *testing.T) {
|
|
servers := make([]*model.Server, 45)
|
|
for i := range servers {
|
|
servers[i] = &model.Server{
|
|
Alias: fmt.Sprintf("server-%02d", i+1),
|
|
DisplayName: fmt.Sprintf("Server %02d", i+1),
|
|
Host: fmt.Sprintf("host-%02d.example.org", i+1),
|
|
Port: 22,
|
|
User: "mirivlad",
|
|
AuthMethod: model.AuthKey,
|
|
}
|
|
}
|
|
|
|
m := New(servers)
|
|
updated, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 18})
|
|
model := updated.(*tuiModel)
|
|
for i := 0; i < 20; i++ {
|
|
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
|
model = updated.(*tuiModel)
|
|
}
|
|
|
|
view := model.View()
|
|
if !strings.Contains(view, "Server 21") {
|
|
t.Fatalf("expected selected server to be visible after navigation:\n%s", view)
|
|
}
|
|
if !strings.Contains(view, "Showing") {
|
|
t.Fatalf("expected range hint for long server list:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestEscClosesGroupListBeforeLeavingForm(t *testing.T) {
|
|
oldGetGroups := GetGroups
|
|
GetGroups = func() ([]string, error) {
|
|
return []string{"prod", "stage"}, nil
|
|
}
|
|
defer func() { GetGroups = oldGetGroups }()
|
|
|
|
m := &tuiModel{
|
|
screen: screenForm,
|
|
form: newFormModel(80, 24),
|
|
}
|
|
m.form.focusIdx = 8
|
|
m.form.updateFocus()
|
|
|
|
updated, _ := m.updateForm(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
m = updated.(*tuiModel)
|
|
if !m.form.showGroupList {
|
|
t.Fatal("expected / on the group field to open the group list")
|
|
}
|
|
|
|
updated, _ = m.updateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
|
|
if m.screen != screenForm {
|
|
t.Fatalf("expected Esc to keep the user in the form, got screen %v", m.screen)
|
|
}
|
|
if m.form == nil {
|
|
t.Fatal("expected form to remain open")
|
|
}
|
|
if m.form.showGroupList {
|
|
t.Fatal("expected Esc to close only the group list")
|
|
}
|
|
}
|
|
|
|
func TestEscClosesAuthMethodListBeforeLeavingForm(t *testing.T) {
|
|
m := &tuiModel{
|
|
screen: screenForm,
|
|
form: newFormModel(80, 24),
|
|
}
|
|
m.form.focusIdx = 5
|
|
m.form.updateFocus()
|
|
|
|
updated, _ := m.updateForm(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
m = updated.(*tuiModel)
|
|
if !m.form.showAuthList {
|
|
t.Fatal("expected / on the auth method field to open the auth method list")
|
|
}
|
|
|
|
updated, _ = m.updateForm(tea.KeyMsg{Type: tea.KeyEsc})
|
|
m = updated.(*tuiModel)
|
|
|
|
if m.screen != screenForm {
|
|
t.Fatalf("expected Esc to keep the user in the form, got screen %v", m.screen)
|
|
}
|
|
if m.form == nil {
|
|
t.Fatal("expected form to remain open")
|
|
}
|
|
if m.form.showAuthList {
|
|
t.Fatal("expected Esc to close only the auth method list")
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodListSelectsValue(t *testing.T) {
|
|
fm := newFormModel(80, 24)
|
|
fm.focusIdx = 5
|
|
fm.updateFocus()
|
|
|
|
updated, _ := fm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
fm = updated.(*formModel)
|
|
if !fm.showAuthList {
|
|
t.Fatal("expected / on the auth method field to open the auth method list")
|
|
}
|
|
|
|
fm.authList.Select(2)
|
|
updated, _ = fm.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
|
fm = updated.(*formModel)
|
|
|
|
if fm.showAuthList {
|
|
t.Fatal("expected Enter to close auth method list")
|
|
}
|
|
if got := fm.inputs[5].Value(); got != string(model.AuthKeyPassphrase) {
|
|
t.Fatalf("expected auth method %q, got %q", model.AuthKeyPassphrase, got)
|
|
}
|
|
}
|
|
|
|
func TestAuthMethodListViewShowsAllOptions(t *testing.T) {
|
|
fm := newFormModel(80, 12)
|
|
fm.focusIdx = 5
|
|
fm.updateFocus()
|
|
|
|
updated, _ := fm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
fm = updated.(*formModel)
|
|
|
|
view := fm.View()
|
|
authPos := strings.Index(view, "Auth Method")
|
|
listPos := strings.Index(view, "Select auth method")
|
|
if authPos < 0 || listPos < 0 {
|
|
t.Fatalf("expected auth field and auth list title in view\nview:\n%s", view)
|
|
}
|
|
if listPos < authPos {
|
|
t.Fatalf("expected auth method list after auth field\nview:\n%s", view)
|
|
}
|
|
if between := view[authPos:listPos]; strings.Contains(between, "Identity File") {
|
|
t.Fatalf("expected auth method list to render directly under auth field\nview:\n%s", view)
|
|
}
|
|
if strings.Contains(view, "│") {
|
|
t.Fatalf("expected compact auth method dropdown without default list border\nview:\n%s", view)
|
|
}
|
|
for _, method := range []model.AuthMethod{
|
|
model.AuthPassword,
|
|
model.AuthKey,
|
|
model.AuthKeyPassphrase,
|
|
model.AuthAgent,
|
|
} {
|
|
if !strings.Contains(view, string(method)) {
|
|
t.Fatalf("expected auth method list view to contain %q\nview:\n%s", method, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGroupListViewRendersDirectlyUnderGroupField(t *testing.T) {
|
|
oldGetGroups := GetGroups
|
|
GetGroups = func() ([]string, error) {
|
|
return []string{"KP", "MY"}, nil
|
|
}
|
|
defer func() { GetGroups = oldGetGroups }()
|
|
|
|
fm := newFormModel(80, 24)
|
|
fm.focusIdx = 8
|
|
fm.updateFocus()
|
|
|
|
updated, _ := fm.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
|
|
fm = updated.(*formModel)
|
|
|
|
view := fm.View()
|
|
groupPos := strings.Index(view, "Group")
|
|
listPos := strings.Index(view, "Select group")
|
|
if groupPos < 0 || listPos < 0 {
|
|
t.Fatalf("expected group field and group list title in view\nview:\n%s", view)
|
|
}
|
|
if listPos < groupPos {
|
|
t.Fatalf("expected group list after group field\nview:\n%s", view)
|
|
}
|
|
if between := view[groupPos:listPos]; strings.Contains(between, "Password") {
|
|
t.Fatalf("expected group dropdown to render before password field\nview:\n%s", view)
|
|
}
|
|
if strings.Contains(view, "│") {
|
|
t.Fatalf("expected compact group dropdown without default list border\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestSelectableFieldHintsAreVisible(t *testing.T) {
|
|
oldGetGroups := GetGroups
|
|
GetGroups = func() ([]string, error) {
|
|
return []string{"KP"}, nil
|
|
}
|
|
defer func() { GetGroups = oldGetGroups }()
|
|
|
|
fm := newFormModel(80, 24)
|
|
view := fm.View()
|
|
for _, want := range []string{"Auth Method (/ pick)", "Group (/ pick)", "pick list"} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected form view to contain selectable-field hint %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEditFormShowsSavedSecretMarker(t *testing.T) {
|
|
oldHasSecret := HasSecret
|
|
HasSecret = func(alias string, secretType string) bool {
|
|
return alias == "prod" && secretType == "ssh_password"
|
|
}
|
|
defer func() { HasSecret = oldHasSecret }()
|
|
|
|
fm := newEditFormModel(&model.Server{
|
|
Alias: "prod",
|
|
Host: "example.org",
|
|
Port: 22,
|
|
User: "root",
|
|
AuthMethod: model.AuthPassword,
|
|
}, 80, 24)
|
|
|
|
view := fm.View()
|
|
if !strings.Contains(view, "secret saved") {
|
|
t.Fatalf("expected edit form to show saved secret marker\nview:\n%s", view)
|
|
}
|
|
if !strings.Contains(view, "leave blank to keep") {
|
|
t.Fatalf("expected edit form to explain blank password keeps saved secret\nview:\n%s", view)
|
|
}
|
|
if strings.Count(view, "secret saved") != 1 {
|
|
t.Fatalf("expected saved secret marker to appear once\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestFormViewUsesSectionsAndStableLabels(t *testing.T) {
|
|
fm := newFormModel(100, 30)
|
|
view := fm.View()
|
|
|
|
for _, want := range []string{
|
|
"Identity",
|
|
"Connection",
|
|
"Authentication",
|
|
"Metadata",
|
|
"Actions",
|
|
"Alias",
|
|
"Display Name",
|
|
"Auth Method",
|
|
"Password / Passphrase",
|
|
} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected form view to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormTestResultDoesNotUpdateSelectedListServer(t *testing.T) {
|
|
oldUpdateTestResult := UpdateTestResult
|
|
oldListServers := ListServers
|
|
defer func() {
|
|
UpdateTestResult = oldUpdateTestResult
|
|
ListServers = oldListServers
|
|
}()
|
|
|
|
updateCalled := false
|
|
UpdateTestResult = func(alias string, status model.TestStatus, testErr string) error {
|
|
updateCalled = true
|
|
return nil
|
|
}
|
|
ListServers = func() ([]*model.Server, error) {
|
|
t.Fatal("form test result should not reload server list")
|
|
return nil, nil
|
|
}
|
|
|
|
selected := &model.Server{Alias: "selected", Host: "example.org", Port: 22, User: "root"}
|
|
m := New([]*model.Server{selected})
|
|
m.screen = screenForm
|
|
m.form = newFormModel(80, 24)
|
|
m.list = list.New([]list.Item{serverItem{server: selected}}, list.NewDefaultDelegate(), 80, 20)
|
|
|
|
updated, cmd := m.Update(testDoneMsg{ok: true})
|
|
m = updated.(*tuiModel)
|
|
|
|
if cmd != nil {
|
|
t.Fatal("form test result should not return a reload command")
|
|
}
|
|
if updateCalled {
|
|
t.Fatal("form test result should not update the selected list server")
|
|
}
|
|
if m.form == nil || m.form.testResult != "Connection OK." {
|
|
t.Fatal("expected form to keep its test result")
|
|
}
|
|
}
|
|
|
|
func TestServerFormBuildsStartupCommandAndTags(t *testing.T) {
|
|
fm := newFormModel(100, 30)
|
|
fm.inputs[0].SetValue("prod")
|
|
fm.inputs[2].SetValue("prod.example.org")
|
|
fm.inputs[3].SetValue("22")
|
|
fm.inputs[4].SetValue("root")
|
|
fm.inputs[5].SetValue(string(model.AuthKey))
|
|
fm.inputs[10].SetValue("tmux attach -t ops")
|
|
fm.inputs[11].SetValue("prod, web, prod")
|
|
|
|
server := fm.buildServer()
|
|
if server.StartupCommand != "tmux attach -t ops" {
|
|
t.Fatalf("startup command = %q", server.StartupCommand)
|
|
}
|
|
if got := strings.Join(server.Tags, ","); got != "prod,web" {
|
|
t.Fatalf("tags = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestInsertTogglesServerSelection(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
{Alias: "two", Host: "two.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
|
|
updated, _ := m.updateList(tea.KeyMsg{Type: tea.KeyInsert})
|
|
model := updated.(*tuiModel)
|
|
if !model.selected["one"] {
|
|
t.Fatal("expected Insert to select current server")
|
|
}
|
|
if model.list.Index() != 1 {
|
|
t.Fatalf("expected Insert to advance to next server, index = %d", model.list.Index())
|
|
}
|
|
|
|
updated, _ = model.updateList(tea.KeyMsg{Type: tea.KeyInsert})
|
|
model = updated.(*tuiModel)
|
|
if !model.selected["two"] {
|
|
t.Fatal("expected second Insert to select next server")
|
|
}
|
|
}
|
|
|
|
func TestCtrlROpensTemplatePicker(t *testing.T) {
|
|
oldListCommandTemplates := ListCommandTemplates
|
|
defer func() { ListCommandTemplates = oldListCommandTemplates }()
|
|
ListCommandTemplates = func() ([]*model.CommandTemplate, error) {
|
|
return []*model.CommandTemplate{{Name: "uptime", Command: "uptime"}}, nil
|
|
}
|
|
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
updated, cmd := m.updateList(tea.KeyMsg{Type: tea.KeyCtrlR})
|
|
model := updated.(*tuiModel)
|
|
if model.screen != screenTemplatePicker {
|
|
t.Fatalf("expected Ctrl+R to open template picker, screen = %v", model.screen)
|
|
}
|
|
if cmd == nil {
|
|
t.Fatal("expected Ctrl+R to load templates")
|
|
}
|
|
}
|
|
|
|
func TestShiftEnterDoesNotOpenTemplatePicker(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
|
|
updated, _ := m.updateList(tea.KeyMsg{Type: tea.KeyEnter})
|
|
model := updated.(*tuiModel)
|
|
if model.screen == screenTemplatePicker {
|
|
t.Fatal("Shift+Enter fallback should not be wired; Ctrl+R is the template shortcut")
|
|
}
|
|
}
|
|
|
|
func TestTemplatePickerForegroundUsesSelectedServers(t *testing.T) {
|
|
servers := []*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
{Alias: "two", Host: "two.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
}
|
|
m := New(servers)
|
|
m.selected["one"] = true
|
|
m.selected["two"] = true
|
|
m.pendingTemplate = &model.CommandTemplate{Name: "uptime", Command: "uptime"}
|
|
m.screen = screenTemplateMode
|
|
|
|
updated, cmd := m.updateTemplateMode(tea.KeyMsg{Type: tea.KeyEnter})
|
|
model := updated.(*tuiModel)
|
|
if cmd == nil {
|
|
t.Fatal("expected foreground template mode to return a command")
|
|
}
|
|
msg := cmd()
|
|
req, ok := msg.(templateRunRequestMsg)
|
|
if !ok {
|
|
t.Fatalf("expected templateRunRequestMsg, got %T", msg)
|
|
}
|
|
if len(req.servers) != 2 || req.command != "uptime" || req.templateName != "uptime" {
|
|
t.Fatalf("unexpected template request: %#v", req)
|
|
}
|
|
model.Update(req)
|
|
if model.Result() == nil || model.Result().Action != "run_template_foreground" {
|
|
t.Fatalf("expected foreground result, got %#v", model.Result())
|
|
}
|
|
}
|
|
|
|
func TestTemplateModeUsesControlShortcuts(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.pendingTemplate = &model.CommandTemplate{Name: "uptime", Command: "uptime"}
|
|
m.screen = screenTemplateMode
|
|
|
|
_, cmd := m.updateTemplateMode(tea.KeyMsg{Type: tea.KeyCtrlF})
|
|
if cmd == nil {
|
|
t.Fatal("expected Ctrl+F to start foreground run")
|
|
}
|
|
|
|
called := false
|
|
oldRunTemplateBackground := RunTemplateBackground
|
|
RunTemplateBackground = func(server *model.Server, command string) (string, error) {
|
|
called = true
|
|
return "ok", nil
|
|
}
|
|
defer func() { RunTemplateBackground = oldRunTemplateBackground }()
|
|
|
|
_, cmd = m.updateTemplateMode(tea.KeyMsg{Type: tea.KeyCtrlB})
|
|
if cmd == nil {
|
|
t.Fatal("expected Ctrl+B to start background run")
|
|
}
|
|
cmd()
|
|
if !called {
|
|
t.Fatal("expected Ctrl+B command to call background runner")
|
|
}
|
|
|
|
view := m.viewTemplateMode()
|
|
for _, want := range []string{"Ctrl+F (Enter)", "Foreground", "Ctrl+B", "Background"} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected view to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBackgroundRunReturnsToListAndShowsResultPanel(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.screen = screenTemplateMode
|
|
|
|
updated, _ := m.Update(backgroundRunDoneMsg{results: []templateRunResult{
|
|
{Alias: "one", Output: "Distributor ID:\tDebian\nRelease:\t11"},
|
|
}})
|
|
model := updated.(*tuiModel)
|
|
if model.screen != screenList {
|
|
t.Fatalf("expected background result to return to server list, got screen %v", model.screen)
|
|
}
|
|
|
|
view := model.View()
|
|
for _, want := range []string{"sshkeeper", "Last Background Run", "one", "OK", "Distributor ID:", "Release:"} {
|
|
if !strings.Contains(view, want) {
|
|
t.Fatalf("expected view to contain %q\nview:\n%s", want, view)
|
|
}
|
|
}
|
|
if strings.Contains(view, "Background Results") {
|
|
t.Fatalf("expected inline list panel, not standalone background screen\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestBackgroundRunShowsOutputForSelectedServerInMultiRun(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
{Alias: "two", Host: "two.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.bgResults = []templateRunResult{
|
|
{Alias: "one", Output: "one output"},
|
|
{Alias: "two", Output: "two output"},
|
|
}
|
|
|
|
view := m.View()
|
|
if !strings.Contains(view, "one output") || strings.Contains(view, "two output") {
|
|
t.Fatalf("expected selected server output only\nview:\n%s", view)
|
|
}
|
|
|
|
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
|
|
model := updated.(*tuiModel)
|
|
view = model.View()
|
|
if !strings.Contains(view, "two output") || strings.Contains(view, "one output") {
|
|
t.Fatalf("expected output to follow selected server\nview:\n%s", view)
|
|
}
|
|
}
|
|
|
|
func TestBackgroundOutputLinesArePaddedAndTabsExpanded(t *testing.T) {
|
|
m := New([]*model.Server{
|
|
{Alias: "one", Host: "one.example.org", Port: 22, User: "root", AuthMethod: model.AuthKey},
|
|
})
|
|
m.width = 48
|
|
m.bgResults = []templateRunResult{{Alias: "one", Output: "Distributor ID:\tDebian"}}
|
|
|
|
view := m.View()
|
|
if strings.Contains(view, "\t") {
|
|
t.Fatalf("expected tabs to be expanded\nview:\n%s", view)
|
|
}
|
|
for _, line := range strings.Split(view, "\n") {
|
|
if strings.Contains(line, "Distributor ID:") && len(line) < 48 {
|
|
t.Fatalf("expected output line to be padded to clear stale chars, len=%d line=%q", len(line), line)
|
|
}
|
|
}
|
|
}
|