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) } }