Skip to content

Commit 09fd986

Browse files
fix(tui): fix escape loop in search flow and add regression tests
Esc from SearchInput and Search view now always returns to Dashboard instead of using PrevScreen, which could point to SearchResults and cause an infinite loop. Added PrevScreen reset on SearchResults escape and search input clear when entering search from dashboard. Co-authored fixes from PR #16 that were lost during merge conflict.
1 parent 281bb1e commit 09fd986

File tree

2 files changed

+82
-2
lines changed

2 files changed

+82
-2
lines changed

internal/tui/update.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func (m Model) handleSearchInputKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
234234
return m, nil
235235
case "esc":
236236
m.SearchInput.Blur()
237-
m.Screen = m.PrevScreen
237+
m.Screen = ScreenDashboard
238238
m.Cursor = 0
239239
return m, nil
240240
}
@@ -248,7 +248,7 @@ func (m Model) handleSearchInputKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
248248
func (m Model) handleSearchKeys(key string) (tea.Model, tea.Cmd) {
249249
switch key {
250250
case "esc", "q":
251-
m.Screen = m.PrevScreen
251+
m.Screen = ScreenDashboard
252252
m.Cursor = 0
253253
return m, nil
254254
case "i", "/":

internal/tui/update_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,86 @@ func TestHandleSearchResultsAndObservationDetailRemainingBranches(t *testing.T)
567567
}
568568
}
569569

570+
func TestSearchEscapeFlowNoLoop(t *testing.T) {
571+
// Verifies the full escape chain never loops back:
572+
// Dashboard → Search → Results → ObsDetail → Esc → Results → Esc → Search → Esc → Dashboard
573+
574+
m := New(nil, "")
575+
m.Height = 20
576+
m.SearchResults = []store.SearchResult{{Observation: store.Observation{ID: 42}}}
577+
578+
// Step 1: from SearchResults, enter ObservationDetail — PrevScreen = ScreenSearchResults
579+
m.Screen = ScreenSearchResults
580+
m.Cursor = 0
581+
updatedModel, _ := m.handleSearchResultsKeys("enter")
582+
m = updatedModel.(Model)
583+
if m.PrevScreen != ScreenSearchResults {
584+
t.Fatalf("after enter, PrevScreen should be ScreenSearchResults, got %v", m.PrevScreen)
585+
}
586+
587+
// Step 2: from ObservationDetail, Esc → back to SearchResults (via PrevScreen)
588+
m.Screen = ScreenObservationDetail
589+
m.SelectedObservation = &store.Observation{ID: 42}
590+
updatedModel, _ = m.handleObservationDetailKeys("esc")
591+
m = updatedModel.(Model)
592+
if m.Screen != ScreenSearchResults {
593+
t.Fatalf("esc from ObservationDetail should go to ScreenSearchResults, got %v", m.Screen)
594+
}
595+
596+
// Step 3: from SearchResults, Esc → back to ScreenSearch, PrevScreen reset to Dashboard
597+
m.Screen = ScreenSearchResults
598+
updatedModel, _ = m.handleSearchResultsKeys("esc")
599+
m = updatedModel.(Model)
600+
if m.Screen != ScreenSearch {
601+
t.Fatalf("esc from SearchResults should go to ScreenSearch, got %v", m.Screen)
602+
}
603+
if m.PrevScreen != ScreenDashboard {
604+
t.Fatalf("esc from SearchResults should reset PrevScreen to ScreenDashboard, got %v", m.PrevScreen)
605+
}
606+
607+
// Step 4: from Search (no input focused), Esc → always Dashboard, never loops
608+
m.Screen = ScreenSearch
609+
updatedModel, _ = m.handleSearchKeys("esc")
610+
m = updatedModel.(Model)
611+
if m.Screen != ScreenDashboard {
612+
t.Fatalf("esc from Search should always go to ScreenDashboard, got %v", m.Screen)
613+
}
614+
615+
// Step 5: from Search input focused, Esc → always Dashboard, never loops
616+
m.Screen = ScreenSearch
617+
m.PrevScreen = ScreenSearchResults // simulate stale PrevScreen — must NOT be used
618+
m.SearchInput.Focus()
619+
updatedModel, _ = m.handleSearchInputKeys(tea.KeyMsg{Type: tea.KeyEscape})
620+
m = updatedModel.(Model)
621+
if m.Screen != ScreenDashboard {
622+
t.Fatalf("esc from SearchInput should always go to ScreenDashboard regardless of PrevScreen, got %v", m.Screen)
623+
}
624+
}
625+
626+
func TestSearchInputClearedOnEnterFromDashboard(t *testing.T) {
627+
// Verifies the search input is cleared each time search is opened from dashboard
628+
m := New(nil, "")
629+
m.Screen = ScreenDashboard
630+
m.SearchInput.SetValue("old query")
631+
632+
// Open via keyboard shortcut "s"
633+
updatedModel, _ := m.handleDashboardKeys("s")
634+
m = updatedModel.(Model)
635+
if m.SearchInput.Value() != "" {
636+
t.Fatalf("search input should be cleared when opening search, got %q", m.SearchInput.Value())
637+
}
638+
639+
// Open via dashboard selection (menu item 0)
640+
m.Screen = ScreenDashboard
641+
m.SearchInput.SetValue("another stale query")
642+
m.Cursor = 0
643+
updatedModel, _ = m.handleDashboardSelection()
644+
m = updatedModel.(Model)
645+
if m.SearchInput.Value() != "" {
646+
t.Fatalf("search input should be cleared on dashboard selection, got %q", m.SearchInput.Value())
647+
}
648+
}
649+
570650
func TestHandleSessionsAndSetupRemainingBranches(t *testing.T) {
571651
fx := newTestFixture(t)
572652
m := New(fx.store, "")

0 commit comments

Comments
 (0)