310 lines
9.2 KiB
Go
310 lines
9.2 KiB
Go
package tui
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestOverlay_ESC_ClosesCompletion(t *testing.T) {
|
|
m := newTestModel(t)
|
|
|
|
// Set up active completion state.
|
|
items := []Completion{
|
|
{Label: "/help", Insert: "/help ", Category: "command"},
|
|
{Label: "/clear", Insert: "/clear ", Category: "command"},
|
|
{Label: "/model", Insert: "/model ", Category: "command"},
|
|
}
|
|
m.completionState = newCompletionState("command", items, true)
|
|
m.completionState.Index = 1
|
|
m.completionState.Selected[0] = true
|
|
m.overlay = OverlayCompletion
|
|
|
|
// Send ESC.
|
|
updated, _ := m.Update(escKey())
|
|
m = updated.(*Model)
|
|
|
|
// Verify completion state is nil.
|
|
if m.isCompletionActive() {
|
|
t.Error("completionState should be nil after ESC")
|
|
}
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_ESC_ClearsInputToPreventRetrigger(t *testing.T) {
|
|
m := newTestModel(t)
|
|
|
|
// Simulate: user typed "/" which triggered completion, then presses ESC.
|
|
m.input.SetValue("/")
|
|
items := []Completion{
|
|
{Label: "/help", Insert: "/help ", Category: "command"},
|
|
{Label: "/clear", Insert: "/clear ", Category: "command"},
|
|
}
|
|
m.completionState = newCompletionState("command", items, false)
|
|
m.overlay = OverlayCompletion
|
|
|
|
// Press ESC to close.
|
|
updated, _ := m.Update(escKey())
|
|
m = updated.(*Model)
|
|
|
|
// Input must be cleared so auto-trigger doesn't reopen.
|
|
if m.input.Value() != "" {
|
|
t.Errorf("ESC should clear input, got %q", m.input.Value())
|
|
}
|
|
if m.isCompletionActive() {
|
|
t.Error("completion should be closed after ESC")
|
|
}
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_ESC_NoRetriggerOnSubsequentUpdate(t *testing.T) {
|
|
m := newTestModel(t)
|
|
|
|
// Simulate: user typed "/" which triggered completion, then presses ESC.
|
|
m.input.SetValue("/")
|
|
items := []Completion{
|
|
{Label: "/help", Insert: "/help ", Category: "command"},
|
|
}
|
|
m.completionState = newCompletionState("command", items, false)
|
|
m.overlay = OverlayCompletion
|
|
|
|
// Press ESC.
|
|
updated, _ := m.Update(escKey())
|
|
m = updated.(*Model)
|
|
|
|
// Send another key event (e.g., a harmless key like 'a') to cycle through Update.
|
|
// This exercises the auto-trigger path at lines 968-972.
|
|
updated, _ = m.Update(charKey('a'))
|
|
m = updated.(*Model)
|
|
|
|
// Completion must NOT have re-opened.
|
|
if m.isCompletionActive() {
|
|
t.Error("completion should not re-trigger after ESC close")
|
|
}
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("overlay should still be OverlayNone, got %d", m.overlay)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_ESC_ClosesHelp(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.overlay = OverlayHelp
|
|
|
|
updated, _ := m.Update(escKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("overlay should be OverlayNone after ESC, got %d", m.overlay)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_HelpDismissal(t *testing.T) {
|
|
t.Run("question_mark_dismisses", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.overlay = OverlayHelp
|
|
|
|
updated, _ := m.Update(charKey('?'))
|
|
m = updated.(*Model)
|
|
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("? should dismiss help overlay, got %d", m.overlay)
|
|
}
|
|
})
|
|
|
|
t.Run("q_dismisses", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.overlay = OverlayHelp
|
|
|
|
updated, _ := m.Update(charKey('q'))
|
|
m = updated.(*Model)
|
|
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("q should dismiss help overlay, got %d", m.overlay)
|
|
}
|
|
})
|
|
|
|
t.Run("other_key_swallowed", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
m.overlay = OverlayHelp
|
|
|
|
updated, _ := m.Update(charKey('a'))
|
|
m = updated.(*Model)
|
|
|
|
if m.overlay != OverlayHelp {
|
|
t.Errorf("'a' should be swallowed, overlay should remain OverlayHelp, got %d", m.overlay)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOverlay_CompletionNavigation(t *testing.T) {
|
|
setup := func(t *testing.T) *Model {
|
|
t.Helper()
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "/help", Insert: "/help "},
|
|
{Label: "/clear", Insert: "/clear "},
|
|
{Label: "/model", Insert: "/model "},
|
|
}
|
|
m.completionState = newCompletionState("command", items, false)
|
|
m.overlay = OverlayCompletion
|
|
return m
|
|
}
|
|
|
|
t.Run("down_moves_index", func(t *testing.T) {
|
|
m := setup(t)
|
|
|
|
updated, _ := m.Update(downKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.completionState.Index != 1 {
|
|
t.Errorf("down from 0 should move to 1, got %d", m.completionState.Index)
|
|
}
|
|
})
|
|
|
|
t.Run("up_at_zero_stays", func(t *testing.T) {
|
|
m := setup(t)
|
|
|
|
updated, _ := m.Update(upKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.completionState.Index != 0 {
|
|
t.Errorf("up at 0 should stay at 0, got %d", m.completionState.Index)
|
|
}
|
|
})
|
|
|
|
t.Run("down_clamped_at_end", func(t *testing.T) {
|
|
m := setup(t)
|
|
m.completionState.Index = 2
|
|
|
|
updated, _ := m.Update(downKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.completionState.Index != 2 {
|
|
t.Errorf("down at last item should stay at 2, got %d", m.completionState.Index)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOverlay_CompletionToggle(t *testing.T) {
|
|
t.Run("tab_toggles_selection_on", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "/a", Insert: "/a "},
|
|
{Label: "/b", Insert: "/b "},
|
|
}
|
|
m.completionState = newCompletionState("attachments", items, true)
|
|
m.overlay = OverlayCompletion
|
|
|
|
updated, _ := m.Update(tabKey())
|
|
m = updated.(*Model)
|
|
|
|
if !m.completionState.Selected[0] {
|
|
t.Error("tab should toggle selection on for index 0")
|
|
}
|
|
})
|
|
|
|
t.Run("tab_toggles_selection_off", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "/a", Insert: "/a "},
|
|
{Label: "/b", Insert: "/b "},
|
|
}
|
|
m.completionState = newCompletionState("attachments", items, true)
|
|
m.completionState.Selected[0] = true
|
|
m.overlay = OverlayCompletion
|
|
|
|
updated, _ := m.Update(tabKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.completionState.Selected[0] {
|
|
t.Error("tab should toggle selection off for index 0")
|
|
}
|
|
})
|
|
|
|
t.Run("nil_selected_no_panic", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "/a", Insert: "/a "},
|
|
}
|
|
m.completionState = newCompletionState("command", items, false)
|
|
// Selected is nil for single-select mode
|
|
m.overlay = OverlayCompletion
|
|
|
|
// Should not panic.
|
|
updated, _ := m.Update(tabKey())
|
|
_ = updated.(*Model)
|
|
})
|
|
}
|
|
|
|
func TestOverlay_CompletionAccept(t *testing.T) {
|
|
t.Run("single_select", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "/help", Insert: "/help "},
|
|
{Label: "/clear", Insert: "/clear "},
|
|
}
|
|
m.completionState = newCompletionState("command", items, false)
|
|
m.completionState.Index = 1
|
|
m.overlay = OverlayCompletion
|
|
|
|
updated, _ := m.Update(enterKey())
|
|
m = updated.(*Model)
|
|
|
|
if m.input.Value() != "/clear " {
|
|
t.Errorf("input should be '/clear ', got %q", m.input.Value())
|
|
}
|
|
if m.isCompletionActive() {
|
|
t.Error("completion should be closed after accept")
|
|
}
|
|
if m.overlay != OverlayNone {
|
|
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
|
|
}
|
|
})
|
|
|
|
t.Run("multi_select_with_selections", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "@file1", Insert: "@file1 "},
|
|
{Label: "@file2", Insert: "@file2 "},
|
|
{Label: "@file3", Insert: "@file3 "},
|
|
}
|
|
m.completionState = newCompletionState("attachments", items, true)
|
|
m.completionState.Selected[0] = true
|
|
m.completionState.Selected[2] = true
|
|
m.overlay = OverlayCompletion
|
|
|
|
updated, _ := m.Update(enterKey())
|
|
m = updated.(*Model)
|
|
|
|
val := m.input.Value()
|
|
// Selected items 0 and 2 should be joined.
|
|
if val == "" {
|
|
t.Error("input should not be empty with multi-select")
|
|
}
|
|
if m.isCompletionActive() {
|
|
t.Error("completion should be closed after accept")
|
|
}
|
|
})
|
|
|
|
t.Run("multi_select_empty_fallback", func(t *testing.T) {
|
|
m := newTestModel(t)
|
|
items := []Completion{
|
|
{Label: "@file1", Insert: "@file1 "},
|
|
{Label: "@file2", Insert: "@file2 "},
|
|
}
|
|
m.completionState = newCompletionState("attachments", items, true)
|
|
m.completionState.Index = 1
|
|
m.overlay = OverlayCompletion
|
|
|
|
updated, _ := m.Update(enterKey())
|
|
m = updated.(*Model)
|
|
|
|
// Fallback to current item.
|
|
if m.input.Value() != "@file2 " {
|
|
t.Errorf("should fallback to current item, got %q", m.input.Value())
|
|
}
|
|
})
|
|
}
|