mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-09 22:30:41 +02:00
fix: resolve search behavior when cursor is not at top of list
- Fix search mode not triggering properly after navigation - Preserve cursor position during filtering operations - Add comprehensive UI tests for search functionality - Improve search to include user field filtering
This commit is contained in:
parent
44ffa0c31d
commit
9bb5d18f8e
305
internal/ui/search_test.go
Normal file
305
internal/ui/search_test.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTestModel creates a model with test data for testing
|
||||||
|
func createTestModel() Model {
|
||||||
|
hosts := []config.SSHHost{
|
||||||
|
{Name: "server1", Hostname: "server1.example.com", User: "user1"},
|
||||||
|
{Name: "server2", Hostname: "server2.example.com", User: "user2"},
|
||||||
|
{Name: "server3", Hostname: "server3.example.com", User: "user3"},
|
||||||
|
{Name: "web-server", Hostname: "web.example.com", User: "webuser"},
|
||||||
|
{Name: "db-server", Hostname: "db.example.com", User: "dbuser"},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Model{
|
||||||
|
hosts: hosts,
|
||||||
|
filteredHosts: hosts,
|
||||||
|
searchInput: textinput.New(),
|
||||||
|
table: table.New(),
|
||||||
|
searchMode: false,
|
||||||
|
ready: true,
|
||||||
|
width: 80,
|
||||||
|
height: 24,
|
||||||
|
styles: NewStyles(80),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize table with test data
|
||||||
|
m.updateTableColumns()
|
||||||
|
m.updateTableRows()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchModeToggle(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Initially should not be in search mode
|
||||||
|
if m.searchMode {
|
||||||
|
t.Error("Model should not start in search mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate pressing "/" to enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Should now be in search mode
|
||||||
|
if !m.searchMode {
|
||||||
|
t.Error("Model should be in search mode after pressing '/'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The search input should be focused
|
||||||
|
if !m.searchInput.Focused() {
|
||||||
|
t.Error("Search input should be focused in search mode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchFiltering(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Type "server" in search
|
||||||
|
for _, char := range "server" {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should filter to only hosts containing "server"
|
||||||
|
expectedHosts := []string{"server1", "server2", "server3", "web-server", "db-server"}
|
||||||
|
if len(m.filteredHosts) != len(expectedHosts) {
|
||||||
|
t.Errorf("Expected %d filtered hosts, got %d", len(expectedHosts), len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all filtered hosts contain "server"
|
||||||
|
for _, host := range m.filteredHosts {
|
||||||
|
found := false
|
||||||
|
for _, expected := range expectedHosts {
|
||||||
|
if host.Name == expected {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Unexpected host in filtered results: %s", host.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchFilteringSpecific(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Type "web" in search
|
||||||
|
for _, char := range "web" {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should filter to only hosts containing "web"
|
||||||
|
if len(m.filteredHosts) != 1 {
|
||||||
|
t.Errorf("Expected 1 filtered host, got %d", len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.filteredHosts) > 0 && m.filteredHosts[0].Name != "web-server" {
|
||||||
|
t.Errorf("Expected 'web-server', got '%s'", m.filteredHosts[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchClearReturnToOriginal(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
originalHostCount := len(m.hosts)
|
||||||
|
|
||||||
|
// Enter search mode and type something
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Type "web" in search
|
||||||
|
for _, char := range "web" {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have filtered results
|
||||||
|
if len(m.filteredHosts) >= originalHostCount {
|
||||||
|
t.Error("Search should have filtered down the results")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the search by simulating backspace
|
||||||
|
for i := 0; i < 3; i++ { // "web" is 3 characters
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyBackspace}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should return to all hosts
|
||||||
|
if len(m.filteredHosts) != originalHostCount {
|
||||||
|
t.Errorf("Expected %d hosts after clearing search, got %d", originalHostCount, len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCursorPositionAfterFiltering(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Move cursor down to position 2 (third item)
|
||||||
|
m.table.SetCursor(2)
|
||||||
|
initialCursor := m.table.Cursor()
|
||||||
|
|
||||||
|
if initialCursor != 2 {
|
||||||
|
t.Errorf("Expected cursor at position 2, got %d", initialCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Type "web" - this will filter to only 1 result
|
||||||
|
for _, char := range "web" {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor should be reset to 0 since filtered results has only 1 item
|
||||||
|
// and cursor position 2 would be out of bounds
|
||||||
|
if len(m.filteredHosts) == 1 && m.table.Cursor() != 0 {
|
||||||
|
t.Errorf("Expected cursor to be reset to 0 when filtered results are smaller, got %d", m.table.Cursor())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTabSwitchBetweenSearchAndTable(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
if !m.searchMode {
|
||||||
|
t.Error("Should be in search mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press Tab to switch to table
|
||||||
|
keyMsg = tea.KeyMsg{Type: tea.KeyTab}
|
||||||
|
newModel, _ = m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
if m.searchMode {
|
||||||
|
t.Error("Should not be in search mode after Tab")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press Tab again to switch back to search
|
||||||
|
keyMsg = tea.KeyMsg{Type: tea.KeyTab}
|
||||||
|
newModel, _ = m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
if !m.searchMode {
|
||||||
|
t.Error("Should be in search mode after second Tab")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnterExitsSearchMode(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
if !m.searchMode {
|
||||||
|
t.Error("Should be in search mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press Enter to exit search mode
|
||||||
|
keyMsg = tea.KeyMsg{Type: tea.KeyEnter}
|
||||||
|
newModel, _ = m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
if m.searchMode {
|
||||||
|
t.Error("Should not be in search mode after Enter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchModeDoesNotTriggerOnEmptyInput(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
originalHostCount := len(m.hosts)
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// At this point, filteredHosts should still be the same as the original hosts
|
||||||
|
// because entering search mode should not trigger filtering with empty input
|
||||||
|
if len(m.filteredHosts) != originalHostCount {
|
||||||
|
t.Errorf("Expected %d hosts when entering search mode, got %d", originalHostCount, len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchByHostname(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Search by hostname part "example.com"
|
||||||
|
searchTerm := "example.com"
|
||||||
|
for _, char := range searchTerm {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All hosts should match since they all have "example.com" in hostname
|
||||||
|
if len(m.filteredHosts) != len(m.hosts) {
|
||||||
|
t.Errorf("Expected all %d hosts to match hostname search, got %d", len(m.hosts), len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchByUser(t *testing.T) {
|
||||||
|
m := createTestModel()
|
||||||
|
|
||||||
|
// Enter search mode
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
|
||||||
|
// Search by user "user1"
|
||||||
|
searchTerm := "user1"
|
||||||
|
for _, char := range searchTerm {
|
||||||
|
keyMsg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{char}}
|
||||||
|
newModel, _ := m.Update(keyMsg)
|
||||||
|
m = newModel.(Model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only server1 should match
|
||||||
|
if len(m.filteredHosts) != 1 {
|
||||||
|
t.Errorf("Expected 1 host to match user search, got %d", len(m.filteredHosts))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.filteredHosts) > 0 && m.filteredHosts[0].Name != "server1" {
|
||||||
|
t.Errorf("Expected 'server1' to match user search, got '%s'", m.filteredHosts[0].Name)
|
||||||
|
}
|
||||||
|
}
|
@ -57,6 +57,12 @@ func (m Model) filterHosts(query string) []config.SSHHost {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the user
|
||||||
|
if strings.Contains(strings.ToLower(host.User), query) {
|
||||||
|
filtered = append(filtered, host)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check the tags
|
// Check the tags
|
||||||
for _, tag := range host.Tags {
|
for _, tag := range host.Tags {
|
||||||
if strings.Contains(strings.ToLower(tag), query) {
|
if strings.Contains(strings.ToLower(tag), query) {
|
||||||
|
@ -364,6 +364,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.updateTableStyles()
|
m.updateTableStyles()
|
||||||
m.table.Blur()
|
m.table.Blur()
|
||||||
m.searchInput.Focus()
|
m.searchInput.Focus()
|
||||||
|
// Don't trigger filtering when entering search mode - wait for user input
|
||||||
return m, textinput.Blink
|
return m, textinput.Blink
|
||||||
}
|
}
|
||||||
case "tab":
|
case "tab":
|
||||||
@ -381,6 +382,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.updateTableStyles()
|
m.updateTableStyles()
|
||||||
m.table.Blur()
|
m.table.Blur()
|
||||||
m.searchInput.Focus()
|
m.searchInput.Focus()
|
||||||
|
// Don't trigger filtering when switching to search mode
|
||||||
return m, textinput.Blink
|
return m, textinput.Blink
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
@ -619,12 +621,17 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||||
// Update filtered hosts only if the search value has changed
|
// Update filtered hosts only if the search value has changed
|
||||||
if m.searchInput.Value() != oldValue {
|
if m.searchInput.Value() != oldValue {
|
||||||
|
currentCursor := m.table.Cursor()
|
||||||
if m.searchInput.Value() != "" {
|
if m.searchInput.Value() != "" {
|
||||||
m.filteredHosts = m.filterHosts(m.searchInput.Value())
|
m.filteredHosts = m.filterHosts(m.searchInput.Value())
|
||||||
} else {
|
} else {
|
||||||
m.filteredHosts = m.sortHosts(m.hosts)
|
m.filteredHosts = m.sortHosts(m.hosts)
|
||||||
}
|
}
|
||||||
m.updateTableRows()
|
m.updateTableRows()
|
||||||
|
// If the current cursor position is beyond the filtered results, reset to 0
|
||||||
|
if currentCursor >= len(m.filteredHosts) && len(m.filteredHosts) > 0 {
|
||||||
|
m.table.SetCursor(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.table, cmd = m.table.Update(msg)
|
m.table, cmd = m.table.Update(msg)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user