Files
proxy/internal/tui/help_dialog_test.go
T

220 lines
9.2 KiB
Go

package tui
import (
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"proxyctl/internal/app"
"proxyctl/internal/profile"
)
func TestCompleteModelContextualHelpOpensOverEveryPageAndRestoresPageState(t *testing.T) {
module := &recordingModule{snapshot: app.Snapshot{
ActiveProfile: "office",
Profiles: []profile.Profile{
{Name: "office", Mode: profile.SingleMode, SingleURL: "http://office.example:8080"},
{Name: "remote", Mode: profile.SingleMode, SingleURL: "http://remote.example:8080"},
},
Targets: operationSnapshot().Targets,
}}
t.Run("dashboard focus and Target selection", func(t *testing.T) {
model := resizedModel(t, module, themeModeNoColor, 120, 36)
model = updateKey(model, "down")
model = updateKey(model, "tab")
model = updateKey(model, "down")
model = updateKey(model, "?")
view := model.View()
assertUsableFrame(t, view, 120, 36, "Operations Console", "Contextual Help", "Dashboard", "Target details focus", "APT", "Disable")
model = updateKey(model, " ")
if len(module.intents) != 0 {
t.Fatalf("input in Help acted on the dashboard: %#v", module.intents)
}
model = updateKey(model, "esc")
if model.page != mainPage || model.selectedTargetID != "apt" || model.dashboardFocus != targetDetailsPane || model.detailsOffset != 1 {
t.Fatalf("closing Help did not restore dashboard state: page=%d target=%q focus=%d offset=%d", model.page, model.selectedTargetID, model.dashboardFocus, model.detailsOffset)
}
})
t.Run("Profile management selection and viewport", func(t *testing.T) {
model := resizedModel(t, module, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "down")
model = updateKey(model, "pgdown")
model = updateKey(model, "?")
assertUsableFrame(t, model.View(), 80, 24, "Proxy Profile management", "Contextual Help", "Profile management", "remote", "Edit selected Profile")
model = updateKey(model, "q")
if model.page != profilesPage || model.selectedProfileName != "remote" || model.profileDetailsOffset != 5 {
t.Fatalf("closing Help did not restore management state: page=%d Profile=%q offset=%d", model.page, model.selectedProfileName, model.profileDetailsOffset)
}
})
t.Run("Profile form field focus", func(t *testing.T) {
model := resizedModel(t, module, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "e")
model = updateKey(model, "tab")
model = updateKey(model, "?")
assertUsableFrame(t, model.View(), 80, 24, "Edit Proxy Profile", "Contextual Help", "Profile form", "Single proxy URL field")
model = updateKey(model, "esc")
if model.page != formPage || model.form.field != 1 || model.form.profile.Name != "office" {
t.Fatalf("closing Help did not restore form state: page=%d field=%d Profile=%q", model.page, model.form.field, model.form.profile.Name)
}
})
}
func TestCompleteModelLongHelpScrollsAndSurvivesResize(t *testing.T) {
model := resizedModel(t, responsiveLayoutModule(), themeModeNoColor, 60, 16)
model = updateKey(model, "?")
initial := model.View()
assertUsableFrame(t, initial, 60, 16, "Contextual Help", "Dashboard")
if strings.Contains(initial, "Global exit safety") {
t.Fatalf("long Help unexpectedly exposed its final section before scrolling:\n%s", initial)
}
for range 6 {
model = updateKey(model, "pgdown")
}
scrolled := model.View()
assertUsableFrame(t, scrolled, 60, 16, "Global exit safety", "Ctrl+C")
if scrolled == initial {
t.Fatal("PgDn did not scroll Help")
}
model = resize(model, 120, 36)
model = resize(model, 60, 16)
assertUsableFrame(t, model.View(), 60, 16, "Global exit safety")
model = updateKey(model, "home")
assertUsableFrame(t, model.View(), 60, 16, "Dashboard")
}
func TestCompleteModelHelpStacksOverConfirmationAndBlocksUnderlyingInput(t *testing.T) {
module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{Name: "office"}, {Name: "remote"}}}}
model := resizedModel(t, module, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "d")
model = updateKey(model, "?")
assertUsableFrame(t, model.View(), 80, 24, "Contextual Help", "Delete confirmation", "Deletion cannot be undone")
model = updateKey(model, "y")
if len(module.intents) != 0 || !strings.Contains(model.View(), "Contextual Help") {
t.Fatalf("confirmation key leaked through Help: intents=%#v\n%s", module.intents, model.View())
}
model = updateKey(model, "esc")
assertUsableFrame(t, model.View(), 80, 24, "Delete Proxy Profile “office”?", "> Cancel")
model = updateKey(model, "esc")
if len(module.intents) != 0 || strings.Contains(model.View(), "Delete Proxy Profile") {
t.Fatalf("Escape did not close dialogs from the top without mutation: intents=%#v\n%s", module.intents, model.View())
}
}
func TestCompleteModelDialogResizeAndHelpPreserveMeaningfulFocus(t *testing.T) {
module := &profileManagementModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{Name: "office"}, {Name: "remote"}}}}
model := resizedModel(t, module, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "d")
model = updateKey(model, "right")
model = resize(model, 60, 16)
model = updateKey(model, "?")
assertUsableFrame(t, model.View(), 60, 16, "Delete confirmation", "Delete is focused")
model = resize(model, 120, 36)
model = updateKey(model, "esc")
assertUsableFrame(t, model.View(), 120, 36, "> Delete")
updated, command := model.Update(tea.KeyMsg{Type: tea.KeyEnter})
model = updated.(Model)
if command != nil || len(module.intents) != 1 || module.intents[0].Kind != app.DeleteProfile {
t.Fatalf("resize and Help changed confirmation focus: intents=%#v", module.intents)
}
}
func TestCompleteModelNewConfirmationTakesPrecedenceOverOpenHelp(t *testing.T) {
updates := make(chan app.OperationUpdate, 1)
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
model := resizedModel(t, module, themeModeNoColor, 120, 36)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
model = updateKey(model, "?")
updates <- app.ConfirmationOperationUpdate(module.snapshot, app.Confirmation{Title: "Confirm Docker restart", Summary: "Running work may be disrupted."})
close(updates)
model, _ = applyOperationCommand(t, model, command)
view := model.View()
assertUsableFrame(t, view, 120, 36, "Confirm Docker restart", "> Cancel")
if strings.Contains(view, "Contextual Help") {
t.Fatalf("new safety confirmation did not take precedence over older Help:\n%s", view)
}
model = updateKey(model, "esc")
assertUsableFrame(t, model.View(), 120, 36, "Contextual Help", "No mutation is in progress")
model = updateKey(model, "esc")
if strings.Contains(model.View(), "Contextual Help") {
t.Fatal("second Escape did not close the revealed Help dialog")
}
}
func TestCompleteModelGlobalBackQuitAndInterruptSemantics(t *testing.T) {
module := &recordingModule{snapshot: app.Snapshot{Profiles: []profile.Profile{{Name: "office"}}}}
model := resizedModel(t, module, themeModeNoColor, 80, 24)
model = updateKey(model, "m")
model = updateKey(model, "e")
model = updateKey(model, "q")
if model.page != profilesPage {
t.Fatalf("q from form page = %d, want profilesPage", model.page)
}
model = updateKey(model, "q")
if model.page != mainPage {
t.Fatalf("q from management page = %d, want mainPage", model.page)
}
updated, quit := model.Update(keyMessage("q"))
model = updated.(Model)
if quit == nil {
t.Fatal("q from unobscured dashboard did not request exit")
}
if _, ok := quit().(tea.QuitMsg); !ok {
t.Fatal("q from dashboard did not emit tea.Quit")
}
for _, setup := range []func(Model) Model{
func(model Model) Model { return model },
func(model Model) Model { return updateKey(model, "m") },
func(model Model) Model { return updateKey(updateKey(model, "m"), "e") },
func(model Model) Model { return updateKey(model, "?") },
} {
candidate := setup(resizedModel(t, module, themeModeNoColor, 80, 24))
_, command := candidate.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
if command == nil {
t.Fatalf("Ctrl+C from page %d did not request exit", candidate.page)
}
if _, ok := command().(tea.QuitMsg); !ok {
t.Fatalf("Ctrl+C from page %d did not emit tea.Quit", candidate.page)
}
}
}
func TestCompleteModelHelpDuringMutationExplainsAndPreservesDisabledActions(t *testing.T) {
updates := make(chan app.OperationUpdate, 1)
module := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
model := resizedModel(t, module, themeModeNoColor, 60, 16)
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
model = updateKey(model, "?")
for range 3 {
model = updateKey(model, "pgdown")
}
view := model.View()
assertUsableFrame(t, view, 60, 16, "Mutation in progress", "Conflicting mutations are disabled")
model = updateKey(model, "esc")
updated, blocked := model.Update(tea.KeyMsg{Type: tea.KeySpace})
model = updated.(Model)
if blocked != nil || len(module.intents) != 1 || !strings.Contains(model.View(), "Wait for the current mutation to finish") {
t.Fatalf("Help browsing changed mutation serialization: intents=%#v\n%s", module.intents, model.View())
}
close(updates)
_ = command
}